Compare commits

..
35 Commits
Author SHA1 Message Date
baorong 9989aa4de5 bump version: 5.5.0 2026-02-19 22:06:42 +08:00
baorong f983533d59 chore dep: use puppeteer-core because npm install easy to fail 2026-02-19 22:03:32 +08:00
baorong 2182759d47 chore dep: little mistake 2026-02-19 21:43:23 +08:00
baorong 4d1851ddd2 chore deps: upgrade axios to 1.x 2026-02-19 21:30:25 +08:00
baorong 0d82eca735 chore lint: code sth. 2026-02-19 21:28:07 +08:00
baorong cfb6b271ee chore code: clear some warnings and remove MapChart & AirportCoord 2026-02-19 21:19:40 +08:00
baorong 0b97964457 fix i18n: lingui not work 2026-02-18 19:28:50 +08:00
baorong edfbfcf5db chore(code): adapt code to antd v6 2026-02-18 19:28:00 +08:00
baorong e5172432df chore dep: use highcharts-react-official 2026-02-18 19:04:53 +08:00
baorong 741b04f120 chore deps: upgrade lingui to v5 2026-02-18 15:53:34 +08:00
baorong 83f2cc1344 chore dep: upgrade antd v6 2026-02-13 17:56:40 +08:00
baorong 152556d582 chore dep: upgrade to lingui v4 2026-02-13 17:39:53 +08:00
Baorong Li 67fc31a008 Merge pull request #1165 from baocang/codex/find-and-fix-two-bugs
fix: correct typos in build config and demo
2025-05-29 22:23:18 +08:00
Baocang Nie 23eace8350 fix: correct typos in build config and demo 2025-05-28 11:13:32 +08:00
Baorong 93817e4840 fix: dep mis with pnpm 2024-04-13 12:50:58 +08:00
Baorong bb97d982e4 fix: npm run start not work #1152 2024-04-13 12:49:16 +08:00
Baorong Li 700f58015d Merge pull request #1156 from XyanZhang/master
fix: clicking on the sidebar collapse button results in a blank screen.
2023-06-14 15:17:56 +08:00
XyanZhang 872ce3a825 fix: can't scroll when the media is mobile 2023-06-14 14:47:06 +08:00
XyanZhang 385b31e7bb fix: clicking on the sidebar collapse button results in a blank screen. 2023-06-14 14:16:33 +08:00
Baorong Li 3d66a616ff chore readme: change documentation url 2023-02-21 10:36:16 +08:00
superlbr b7e18a1214 fix locale: change locale filepath due to antd5 #1146 2023-01-22 20:44:28 +08:00
Baorong Li 3b0bae9c70 chore umirc: set antd to false since v5 2022-12-22 10:14:27 +08:00
Baorong Li 8b9343a320 Merge pull request #1142 from vagusX/feat/upgrade-antd-v5
feat: upgrade antd v5
2022-11-27 17:32:40 +08:00
vagusx 8618c75459 feat: replace moment to dayjs 2022-11-27 16:29:42 +08:00
vagusx 711d2077e2 feat: import BackTop from FloatButton 2022-11-27 16:17:07 +08:00
vagusx b25646b033 feat: rename modal open 2022-11-27 15:54:25 +08:00
vagusx 02bf7784b1 feat: upgrade antd v5 2022-11-26 22:15:15 +08:00
Baorong Li c164d60643 Merge pull request #1132 from SuperRay3/master
fix: 修复 antd 组件无法国际化的问题
2022-05-23 15:59:15 +08:00
leijialiang 1bd4cfe73c fix: 修复 antd 组件无法国际化的问题 2022-05-23 14:43:09 +08:00
superlbr f4e1eeb21c fix login: form mis password 2021-12-24 15:19:15 +08:00
superlbr 4b5fe62014 fix i18n: t not work use Trans instead 2021-12-23 20:09:35 +08:00
superlbr 36c2be61ae chore readme: add account/password desc. 2021-11-16 16:08:26 +08:00
superlbr e9206a9448 fix dev: loading chunk mf-dep_async failed 2021-08-03 18:31:03 +08:00
superlbr 0bf28879c4 chore umi: enable mfsu 2021-08-03 17:47:27 +08:00
superlbr 7c936712b9 fix dev: #1108 #1109 2021-07-15 08:15:51 +08:00
207 changed files with 20969 additions and 5858 deletions
+6
View File
@@ -1 +1,7 @@
src/**/*-test.js
src/public
src/routes/chart/ECharts/theme
src/routes/chart/highCharts/mapdata
src/locales/_build/
src/locales/**/*.js
docs/**/*.js
+1 -2
View File
@@ -1,8 +1,7 @@
{
"extends": "react-app",
"rules": {
"import/no-anonymous-default-export": 0,
"jsx-a11y/href-no-hash": 0,
"jsx-a11y/href-no-hash": "off",
"no-console": "warn",
"valid-jsdoc": "warn"
}
+1 -4
View File
@@ -5,7 +5,6 @@ npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
pnpm-lock.yaml
# ide
.idea
@@ -21,6 +20,4 @@ pnpm-lock.yaml
# jslingui
src/locales/_build
src/locales/**/*.js
.vscode
src/locales/**/*.js
+145
View File
@@ -0,0 +1,145 @@
// 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'
const { theme } = require('antd')
const mapToken = theme.defaultAlgorithm(theme.defaultSeed)
const v4Token = Object.fromEntries(
Object.entries(mapToken).map(([k, v]) => [k, typeof v === 'string' ? v : String(v)])
)
// 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: false,
// a lower cost way to genereate sourcemap, default is cheap-module-source-map, could save 60% time in dev hotload
devtool: 'eval',
dva: { immer: true },
dynamicImport: {
loading: 'components/Loader/Loader',
},
extraBabelPlugins: [
[
'import',
{
libraryName: 'lodash',
libraryDirectory: '',
camel2DashComponentName: false,
},
'lodash',
],
[
'import',
{
libraryName: '@ant-design/icons',
libraryDirectory: 'es/icons',
camel2DashComponentName: false,
},
'ant-design-icons',
],
[
'macros'
]
],
hash: true,
ignoreMomentLocale: true,
// umi3 comple node_modules by default, could be disable
nodeModulesTransform: {
type: 'none',
exclude: [],
},
// Webpack Configuration
proxy: {
'/api/v1/weather': {
target: 'https://api.seniverse.com/',
changeOrigin: true,
pathRewrite: { '^/api/v1/weather': '/v3/weather' },
},
},
// Theme for antd
// https://ant.design/docs/react/customize-theme
theme: {
...v4Token,
...lessToJs(
fs.readFileSync(path.join(__dirname, './src/themes/default.less'), 'utf8')
)
},
webpack5: {},
mfsu: {},
chainWebpack: function (config, { webpack }) {
!isDevelopment && config.merge({
optimization: {
minimize: false,
splitChunks: {
chunks: 'all',
minSize: 30000,
minChunks: 3,
automaticNameDelimiter: '.',
cacheGroups: {
react: {
name: 'react',
priority: 20,
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
},
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,
},
},
},
},
})
},
}
+1
View File
@@ -65,6 +65,7 @@ npm run start
4. 启动完成后打开浏览器访问 [http://localhost:7000](http://localhost:7000),如果需要更改启动端口,可在 `.env` 文件中配置。
5. 登录账号有2个,一个账号admin 密码admin;另一个账号guest 密码guest
> 更多信息请参考 [使用文档](https://doc.antd-admin.zuiidea.com/#/zh-cn/)。
+2 -1
View File
@@ -22,7 +22,7 @@ An excellent front-end solution for enterprise applications.
</div>
- Preview - [https://antd-admin.zuiidea.com](https://antd-admin.zuiidea.com)
- Documentation - [https://doc.antd-admin.zuiidea.com](https://doc.antd-admin.zuiidea.com)
- Documentation - [https://superlbr.gitee.io/antd-admin](https://superlbr.gitee.io/antd-admin)
- FAQ - [https://doc.antd-admin.zuiidea.com/#/faq](https://doc.antd-admin.zuiidea.com/#/faq)
- ChangeLog - [https://doc.antd-admin.zuiidea.com/#/change-log](https://doc.antd-admin.zuiidea.com/#/change-log)
@@ -65,6 +65,7 @@ npm run start
4. After the startup is complete, open a browser and visit [http://localhost:7000](http://localhost:7000), If you need to change the startup port, you can configure it in the `.env` file.
5. For login page, there're two account. Username: admin Password: admin / Username: guest Password: guest
> More instructions at [documentation](https://doc.antd-admin.zuiidea.com)。
-5
View File
@@ -1,5 +0,0 @@
const config = {
publicPath: 'https://cdn.antd-admin.zuiidea.com/',
}
export default config
-61
View File
@@ -1,61 +0,0 @@
/**
* 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
@@ -1,7 +0,0 @@
export default {
'/api/v1/weather': {
target: 'https://api.seniverse.com/',
changeOrigin: true,
pathRewrite: { '^/api/v1/weather': '/v3/weather' },
},
}
-13
View File
@@ -1,13 +0,0 @@
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
+1 -1
View File
@@ -1,3 +1,3 @@
module.exports = {
testURL: 'http://localhost:8000'
testURL: 'http://localhost:8000',
}
+21
View File
@@ -0,0 +1,21 @@
import { formatter } from "@lingui/format-json"
export default {
fallbackLocales: {
default: "en",
},
sourceLocale: "en",
locales: ["en", "zh", "pt-br"],
catalogs: [
{
path: "src/locales/{locale}/messages",
include: [
"src/pages",
"src/layouts",
"src/components",
"src/layouts",
],
},
],
format: formatter({ style: "minimal" }),
}
+6 -13
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,19 +30,9 @@ 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`
}
export const waitTime = (time: number = 300) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time)
})
return `//image.zuiidea.com/${avatarList[randomNumber(0, avatarList.length - 1)]}?imageView2/1/w/200/h/200/format/webp/q/75|imageslim`
}
export const Constant = {
@@ -64,3 +54,6 @@ export const Constant = {
sky: '#c1e0fc',
},
}
export Mock from 'mockjs'
export qs from 'qs'
-4008
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,5 +1,4 @@
import Mock from 'mockjs'
import { Constant } from './utils'
import { Mock, Constant } from './_utils'
const { ApiPrefix, Color } = Constant
@@ -136,7 +135,7 @@ const Dashboard = Mock.mock({
],
})
export default {
module.exports = {
[`GET ${ApiPrefix}/dashboard`](req, res) {
res.json(Dashboard)
},
-2
View File
@@ -1,2 +0,0 @@
declare module 'mockjs'
declare module 'qs'
+67
View File
@@ -0,0 +1,67 @@
import { Mock, Constant } from './_utils'
const { ApiPrefix } = Constant
let postId = 0
const database = Mock.mock({
'data|100': [
{
id() {
postId += 1
return postId + 10000
},
'status|1-2': 1,
title: '@title',
author: '@last',
categories: '@word',
tags: '@word',
'views|10-200': 1,
'comments|10-200': 1,
visibility: () => {
return Mock.mock(
'@pick(["Public",' + '"Password protected", ' + '"Private"])'
)
},
date: '@dateTime',
image() {
return Mock.Random.image(
'100x100',
Mock.Random.color(),
'#757575',
'png',
this.author.substr(0, 1)
)
},
},
],
}).data
module.exports = {
[`GET ${ApiPrefix}/posts`](req, res) {
const { query } = req
let { pageSize, page, ...other } = query
pageSize = pageSize || 10
page = page || 1
let newData = database
for (let key in other) {
if ({}.hasOwnProperty.call(other, key)) {
newData = newData.filter(item => {
if ({}.hasOwnProperty.call(item, key)) {
return (
String(item[key])
.trim()
.indexOf(decodeURI(other[key]).trim()) > -1
)
}
return true
})
}
}
res.status(200).json({
data: newData.slice((page - 1) * pageSize, page * pageSize),
total: newData.length,
})
},
}
+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',
},
]
export default {
module.exports = {
[`GET ${ApiPrefix}/routes`](req, res) {
res.status(200).json(database)
},
+35 -47
View File
@@ -1,10 +1,9 @@
import Mock from 'mockjs'
import { Mock, Constant, randomAvatar } from './_utils'
import qs from 'qs'
import { Constant, randomAvatar, waitTime } from './utils'
const { ApiPrefix } = Constant
const usersListData = Mock.mock({
let usersListData = Mock.mock({
'data|80-100': [
{
id: '@id',
@@ -92,28 +91,10 @@ const NOTFOUND = {
documentation_url: 'http://localhost:8000/request',
}
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()
module.exports = {
[`POST ${ApiPrefix}/user/login`](req, res) {
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()
@@ -132,27 +113,33 @@ export default {
}
},
async [`GET ${ApiPrefix}/user/logout`](req, res) {
await waitTime()
[`GET ${ApiPrefix}/user/logout`](req, res) {
res.clearCookie('token')
res.status(200).end()
},
async [`GET ${ApiPrefix}/user`](req, res) {
await waitTime()
const token = authorized(req, res)
if (!token) {
[`GET ${ApiPrefix}/user`](req, res) {
const cookie = req.headers.cookie || ''
const cookies = qs.parse(cookie.replace(/\s/g, ''), { delimiter: ';' })
const response = {}
let user = {}
if (!cookies.token) {
res.status(200).send({ message: 'Not Login' })
return
}
const userItem = adminUsers.find((_) => _.id === token.id)
if (userItem) {
const { password, ...rest } = userItem
res.json(rest)
} else {
res.status(401).send({ message: 'Not Login' })
const token = JSON.parse(cookies.token)
if (token) {
response.success = token.deadline > new Date().getTime()
}
if (response.success) {
const userItem = adminUsers.find(_ => _.id === token.id)
if (userItem) {
const { password, ...other } = userItem
user = other
}
}
response.user = user
res.json(response)
},
[`GET ${ApiPrefix}/users`](req, res) {
@@ -164,10 +151,10 @@ export default {
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()
@@ -179,8 +166,9 @@ export default {
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
@@ -189,14 +177,14 @@ export default {
}
res.status(200).json({
list: newData.slice((page - 1) * pageSize, page * pageSize),
data: newData.slice((page - 1) * pageSize, page * pageSize),
total: newData.length,
})
},
[`POST ${ApiPrefix}/users/delete`](req, res) {
const { ids = [] } = req.body
database = database.filter((item) => !ids.some((_) => _ === item.id))
const { ids=[] } = req.body
database = database.filter(item => !ids.some(_ => _ === item.id))
res.status(204).end()
},
@@ -233,7 +221,7 @@ export default {
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)
@@ -245,7 +233,7 @@ export default {
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)
+75 -67
View File
@@ -1,85 +1,96 @@
{
"name": "antd-admin",
"version": "6.0.0-a.1",
"version": "5.5.0",
"license": "MIT",
"description": "An admin dashboard application demo built upon Ant Design and UmiJS",
"dependencies": {
"@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",
"@ant-design/icons": "^4.8.3",
"@lingui/react": "^5.0.0",
"@rc-component/picker": "^1.9.0",
"antd": "^6.0.0",
"axios": "^1.0.0",
"classnames": "^2.5.1",
"d3-shape": "^2.1.0",
"dayjs": "^1.11.10",
"draft-js": "^0.11.7",
"draftjs-to-html": "^0.9.1",
"draftjs-to-markdown": "^0.6.0",
"dva-model-extend": "^0.1.2",
"echarts": "^5.3.2",
"echarts": "^5.5.0",
"echarts-for-react": "^3.0.2",
"echarts-gl": "^2.0.9",
"echarts-liquidfill": "^3.1.0",
"enquire-js": "^0.2.1",
"highcharts": "^11.2.0",
"highcharts-react-official": "^3.2.3",
"json-format": "^1.0.1",
"lodash": "^4.17.21",
"make-plural": "^7.1.0",
"make-plural": "^7.3.0",
"md5": "^2.3.0",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.2.0",
"path-to-regexp": "^6.2.2",
"prop-types": "^15.8.1",
"qs": "^6.10.3",
"react": "^18.1.0",
"react-countup": "^6.2.0",
"react-dom": "^18.1.0",
"qs": "^6.12.0",
"react": "^18.2.0",
"react-adsense": "^0.1.0",
"react-countup": "^4.4.0",
"react-dom": "^18.2.0",
"react-draft-wysiwyg": "^1.15.0",
"react-helmet": "^6.1.0",
"react-highcharts": "^16.1.0",
"react-perfect-scrollbar": "^1.5.8",
"react-router-dom": "^6.3.0",
"recharts": "^2.1.9",
"recharts": "^2.12.4",
"store": "^2.0.12"
},
"devDependencies": {
"@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/core": "^7.29.0",
"@babel/preset-react": "^7.24.1",
"@lingui/cli": "^5.0.0",
"@lingui/core": "^5.0.0",
"@lingui/format-json": "^5.9.1",
"@umijs/preset-react": "^1.8.32",
"@lingui/macro": "^5.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-dev-expression": "^0.2.3",
"babel-plugin-import": "^1.13.5",
"babel-plugin-import": "^1.13.8",
"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",
"eslint": "^7.32.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.10.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^4.3.8",
"less-vars-to-js": "^1.3.0",
"lint-staged": "^12.4.1",
"puppeteer-core": "^24.37.2",
"lint-staged": "^10.5.4",
"mockjs": "^1.1.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"
"module": "^1.2.5",
"prettier": "^2.8.8",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^21.0.0",
"typescript": "^4.9.5",
"umi": "^3.5.41"
},
"overrides": {
"@babel/core": "^7.29.0"
},
"engines": {
"node": ">= 10.0.0"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"npm run eslint",
"npm run prettier"
"src/**/*.js": [
"eslint --ext .js --fix",
"npm run prettier",
"git add"
],
"*.less": [
"npm run stylelint",
"npm run prettier"
"**/*.less": [
"stylelint --syntax less",
"npm run prettier",
"git add"
]
},
"lingui": {
@@ -96,27 +107,24 @@
{
"path": "src/locales/{locale}/messages",
"include": [
"src"
],
"exclude": [
"**/node_modules/**"
"src/pages",
"src/layouts",
"src/components",
"src/layouts"
]
}
],
"format": "minimal",
"runtimeConfigModule": [
"@/hooks/useIntlProvider",
"i18n"
]
},
"scripts": {
"start": "umi dev",
"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}'",
"analyze": "cross-env ANALYZE=1 umi build",
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider umi build",
"check:model": "umi dva list model",
"lint:js": "eslint --ext .js src",
"lint:style": "stylelint \"src/**/*.less\" --syntax less",
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider umi dev",
"test": "cross-env BABELRC=none umi test",
"prettier": "prettier --write 'src/**/*.{js,less}'",
"test:e2e": "node ./src/e2e/run-puppeteer.js",
"precommit": "lint-staged",
"add-locale": "lingui add-locale",
"extract": "lingui extract",
View File
+31
View File
@@ -0,0 +1,31 @@
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 items = menuOptions.map(item => ({ key: item.key, label: item.name }))
return (
<Dropdown overlay={<Menu onClick={onMenuClick} items={items} />} {...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
@@ -0,0 +1,6 @@
{
"name": "DropOption",
"version": "0.0.0",
"private": true,
"main": "./DropOption.js"
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react'
import { Editor } from 'react-draft-wysiwyg'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import styles from './Editor.less'
const DraftEditor = props => {
return (
<Editor
toolbarClassName={styles.toolbar}
wrapperClassName={styles.wrapper}
editorClassName={styles.editor}
{...props}
/>
)
}
export default DraftEditor
+106
View File
@@ -0,0 +1,106 @@
.wrapper {
height: 500px;
:global {
.rdw-dropdownoption-default {
padding: 6px;
}
.rdw-dropdown-optionwrapper {
box-sizing: content-box;
width: 100%;
border-radius: 0 0 2px 2px;
&:hover {
box-shadow: none;
}
}
.rdw-inline-wrapper {
flex-wrap: wrap;
margin-bottom: 0;
.rdw-option-wrapper {
margin-bottom: 6px;
}
}
.rdw-option-active {
box-shadow: 1px 1px 0 #e8e8e8 inset;
}
.rdw-colorpicker-option {
box-shadow: none;
}
.rdw-colorpicker-modal,
.rdw-embedded-modal,
.rdw-emoji-modal,
.rdw-image-modal,
.rdw-link-modal {
box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05);
}
.rdw-colorpicker-modal,
.rdw-embedded-modal,
.rdw-link-modal {
height: auto;
}
.rdw-emoji-modal {
width: 214px;
}
.rdw-colorpicker-modal {
width: auto;
}
.rdw-embedded-modal-btn,
.rdw-image-modal-btn,
.rdw-link-modal-btn {
height: 32px;
margin-top: 12px;
}
.rdw-embedded-modal-input,
.rdw-embedded-modal-size-input,
.rdw-link-modal-input {
padding: 2px 6px;
height: 32px;
}
.rdw-dropdown-selectedtext {
color: #000;
}
.rdw-dropdown-wrapper,
.rdw-option-wrapper {
min-width: 36px;
transition: all 0.2s ease;
height: 30px;
&:active {
box-shadow: 1px 1px 0 #e8e8e8 inset;
}
&:hover {
box-shadow: 1px 1px 0 #e8e8e8;
}
}
.rdw-dropdown-wrapper {
min-width: 60px;
}
.rdw-editor-main {
box-sizing: border-box;
}
}
.editor {
border: 1px solid #f1f1f1;
padding: 5px;
border-radius: 2px;
height: auto;
min-height: 200px;
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Editor",
"version": "0.0.0",
"private": true,
"main": "./Editor.js"
}
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { TooltipProps } from 'antd/lib/tooltip';
export interface EllipsisTooltipProps extends TooltipProps {
title?: undefined;
overlayStyle?: undefined;
}
export interface EllipsisProps {
tooltip?: boolean | EllipsisTooltipProps;
length?: number;
lines?: number;
style?: React.CSSProperties;
className?: string;
fullWidthRecognition?: boolean;
}
export function getStrFullLength(str: string): number;
export function cutStrByFullLength(str: string, maxLength: number): string;
export default class Ellipsis extends React.Component<EllipsisProps, any> {}
+272
View File
@@ -0,0 +1,272 @@
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 base = tooltip === true ? {} : { ...tooltip };
const styleProp = overlayStyle ? { styles: { root: overlayStyle } } : {};
const props = { ...base, ...styleProp, 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
@@ -0,0 +1,24 @@
.ellipsis {
display: inline-block;
width: 100%;
overflow: hidden;
word-break: break-all;
}
.lines {
position: relative;
.shadow {
position: absolute;
z-index: -999;
display: block;
color: transparent;
opacity: 0;
}
}
.lineClamp {
position: relative;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
}
+17
View File
@@ -0,0 +1,17 @@
---
title: Ellipsis
subtitle: 文本自动省略号
cols: 1
order: 10
---
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
## API
| 参数 | 说明 | 类型 | 默认值 |
| -------------------- | ------------------------------------------------ | ------- | ------ |
| tooltip | 移动到文本展示完整内容的提示 | boolean | - |
| length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - |
| lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1` |
| fullWidthRecognition | 是否将全角字符的长度视为 2 来计算字符串长度 | boolean | - |
+13
View File
@@ -0,0 +1,13 @@
import { getStrFullLength, cutStrByFullLength } from './index';
describe('test calculateShowLength', () => {
it('get full length', () => {
expect(getStrFullLength('一二,a,')).toEqual(8);
});
it('cut str by full length', () => {
expect(cutStrByFullLength('一二,a,', 7)).toEqual('一二,a');
});
it('cut str when length small', () => {
expect(cutStrByFullLength('一22三', 5)).toEqual('一22');
});
});
@@ -1,10 +1,8 @@
import React from 'react'
import styles from './index.less'
import PropTypes from 'prop-types'
import styles from './FilterItem.less'
interface FilterItemProps {
label: string
}
const FilterItem: React.FC<FilterItemProps> = ({ label = '', children }) => {
const FilterItem = ({ label = '', children }) => {
const labelArray = label.split('')
return (
<div className={styles.filterItem}>
@@ -22,4 +20,9 @@ const FilterItem: React.FC<FilterItemProps> = ({ label = '', children }) => {
)
}
FilterItem.propTypes = {
label: PropTypes.string,
children: PropTypes.element.isRequired,
}
export default FilterItem
+6
View File
@@ -0,0 +1,6 @@
{
"name": "FilterItem",
"version": "0.0.0",
"private": true,
"main": "./FilterItem.js"
}
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
export interface GlobalFooterProps {
links?: Array<{
key?: string;
title: React.ReactNode;
href: string;
blankTarget?: boolean;
}>;
copyright?: React.ReactNode;
style?: React.CSSProperties;
className?: string;
}
export default class GlobalFooter extends React.Component<GlobalFooterProps, any> {}
+29
View File
@@ -0,0 +1,29 @@
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'}
rel={link.blankTarget ? 'noreferrer noopener' : undefined}
href={link.href}
>
{link.title}
</a>
))}
</div>
)}
{copyright && <div className={styles.copyright}>{copyright}</div>}
</footer>
);
};
export default GlobalFooter;
+2 -2
View File
@@ -1,4 +1,4 @@
@import '~antd/lib/style/themes/default.less';
/* @import '~antd/lib/style/themes/default.less'; */
.globalFooter {
margin: 48px 0 24px 0;
@@ -13,7 +13,7 @@
transition: all 0.3s;
&:not(:last-child) {
margin-right: 16px;
margin-right: 40px;
}
&:hover {
+15
View File
@@ -0,0 +1,15 @@
---
title: GlobalFooter
subtitle: 全局页脚
cols: 1
order: 7
---
页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
## API
| 参数 | 说明 | 类型 | 默认值 |
| --------- | -------- | ---------------------------------------------------------------- | ------ |
| links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - |
| copyright | 版权信息 | ReactNode | - |
-40
View File
@@ -1,40 +0,0 @@
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
+61
View File
@@ -0,0 +1,61 @@
import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import { Breadcrumb } from 'antd'
import { Link, withRouter } from 'umi'
import { i18n } from '@lingui/core'
import iconMap from 'utils/iconMap'
import { pathToRegexp } from 'path-to-regexp'
import { queryAncestors } from 'utils'
import styles from './Bread.less'
@withRouter
class Bread extends PureComponent {
generateBreadcrumbItems = paths => {
return paths
.map((item, key) => {
if (!item) return null
const content = (
<Fragment>
{item.icon && <span style={{ marginRight: 4 }}>{iconMap[item.icon]}</span>}
{item.name}
</Fragment>
)
return ({
key: item.id || key,
title: paths.length - 1 !== key ? <Link to={item.route || '#'}>{content}</Link> : content,
})
})
.filter(Boolean)
}
render() {
const { routeList, location } = this.props
// Find a route that matches the pathname.
const currentRoute = routeList.find(
(_) => _.route && pathToRegexp(_.route).exec(location.pathname)
)
// Find the breadcrumb navigation of the current route match and all its ancestors.
const paths = currentRoute
? queryAncestors(routeList, currentRoute, 'breadcrumbParentId').reverse()
: [
routeList[0],
{
id: 404,
name: i18n._('Not Found'),
},
]
const items = this.generateBreadcrumbItems(paths)
return <Breadcrumb className={styles.bread} items={items} />
}
}
Bread.propTypes = {
routeList: PropTypes.array,
}
export default Bread
+16
View File
@@ -0,0 +1,16 @@
.bread {
margin-bottom: 24px;
:global {
.ant-breadcrumb {
display: flex;
align-items: center;
}
}
}
@media (max-width: 767px) {
.bread {
margin-bottom: 12px;
}
}
+177
View File
@@ -0,0 +1,177 @@
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 { i18n } from '@lingui/core'
import { getLocale, setLocale } from 'utils'
import dayjs from 'dayjs'
import classnames from 'classnames'
import config from 'config'
import styles from './Header.less'
class Header extends PureComponent {
handleClickMenu = e => {
e.key === 'SignOut' && this.props.onSignOut()
}
render() {
const {
fixed,
avatar,
username,
collapsed,
notifications,
onCollapseChange,
onAllNotificationsRead,
} = this.props
const userMenuItems = [
{
key: 'user',
label: (
<Fragment>
<span style={{ color: '#999', marginRight: 4 }}>
{i18n._('Hi,')}
</span>
<span>{username}</span>
<Avatar style={{ marginLeft: 8 }} src={avatar} />
</Fragment>
),
children: [
{
key: 'SignOut',
label: i18n._('Sign out'),
},
],
},
]
const rightContent = [<Menu key="user" mode="horizontal" onClick={this.handleClickMenu} items={userMenuItems} />]
if (config.i18n) {
const { languages } = config.i18n
const language = getLocale()
const currentLanguage = languages.find(
item => item.key === language
)
const languageItems = [
{
key: 'language',
label: <Avatar size="small" src={currentLanguage.flag} />,
children: languages.map(item => ({
key: item.key,
label: (
<>
<Avatar size="small" style={{ marginRight: 8 }} src={item.flag} />
{item.title}
</>
),
})),
},
]
rightContent.unshift(
<Menu
key="language"
selectedKeys={[currentLanguage.key]}
onClick={data => {
setLocale(data.key)
}}
mode="horizontal"
items={languageItems}
/>
)
}
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: i18n._('You have viewed all notifications.'),
}}
renderItem={item => (
<List.Item className={styles.notificationItem}>
<List.Item.Meta
title={
<Ellipsis tooltip lines={1}>
{item.title}
</Ellipsis>
}
description={dayjs(item.date).fromNow()}
/>
<RightOutlined style={{ fontSize: 10, color: '#ccc' }} />
</List.Item>
)}
/>
{notifications.length ? (
<div
onClick={onAllNotificationsRead}
className={styles.clearButton}
>
{i18n._('Clear notifications')}
</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,
})}
style={{height: 72, backgroundColor: 'white', paddingInline: 0}}
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
+156
View File
@@ -0,0 +1,156 @@
@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;
display: flex;
align-items: center;
}
.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
@@ -0,0 +1,122 @@
import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import { Menu } from 'antd'
import { NavLink, withRouter } from 'umi'
import { pathToRegexp } from 'path-to-regexp'
import { arrayToTree, queryAncestors } from 'utils'
import iconMap from 'utils/iconMap'
import store from 'store'
// Use `items` prop for Menu (avoid JSX children like Menu.Item/SubMenu)
@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)
}
generateMenuItems = data => {
return data.map(item => {
if (item.children) {
return {
key: item.id,
label: (
<Fragment>
{item.icon && iconMap[item.icon]}
<span>{item.name}</span>
</Fragment>
),
children: this.generateMenuItems(item.children),
}
}
return {
key: item.id,
label: (
<NavLink to={item.route || '#'}>
{item.icon && iconMap[item.icon]}
<span>{item.name}</span>
</NavLink>
),
}
})
}
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,
}
const items = this.generateMenuItems(menuTree)
return (
<Menu
mode="inline"
theme={theme}
onOpenChange={this.onOpenChange}
selectedKeys={selectedKeys}
onClick={
isMobile
? () => {
onCollapseChange(true)
}
: undefined
}
items={items}
{...menuProps}
/>
)
}
}
SiderMenu.propTypes = {
menus: PropTypes.array,
theme: PropTypes.string,
isMobile: PropTypes.bool,
onCollapseChange: PropTypes.func,
}
export default SiderMenu
+87
View File
@@ -0,0 +1,87 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Switch, Layout } from 'antd'
import { i18n } from '@lingui/core'
import { BulbOutlined } from '@ant-design/icons'
import ScrollBar from '../ScrollBar'
import { config } from 'utils'
import SiderMenu from './Menu'
import styles from './Sider.less'
class Sider extends PureComponent {
render() {
const {
menus,
theme,
isMobile,
collapsed,
onThemeChange,
onCollapseChange,
} = this.props
return (
<Layout.Sider
width={256}
theme={theme}
breakpoint="lg"
trigger={null}
collapsible
collapsed={collapsed}
onBreakpoint={!isMobile ? onCollapseChange : (broken) => {}}
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 />
{i18n._('Switch Theme')}
</span>
<Switch
onChange={onThemeChange.bind(
this,
theme === 'dark' ? 'light' : 'dark'
)}
defaultChecked={theme === 'dark'}
checkedChildren={i18n._('Dark')}
unCheckedChildren={i18n._('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
@@ -0,0 +1,110 @@
@import '~themes/vars.less';
.sider {
box-shadow: fade(@primary-color, 10%) 0 0 28px 0;
z-index: 10;
:global {
.ant-layout-sider-children {
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
}
.brand {
z-index: 1;
height: 72px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
box-shadow: 0 1px 9px -3px rgba(0, 0, 0, 0.2);
.logo {
display: flex;
align-items: center;
justify-content: center;
img {
width: 36px;
margin-right: 8px;
}
h1 {
vertical-align: text-bottom;
font-size: 16px;
text-transform: uppercase;
display: inline-block;
font-weight: 700;
color: @primary-color;
white-space: nowrap;
margin-bottom: 0;
.text-gradient();
:local {
animation: fadeRightIn 300ms @ease-in-out;
animation-fill-mode: both;
}
}
}
}
.menuContainer {
height: ~'calc(100vh - 120px)';
overflow-x: hidden;
flex: 1;
padding: 24px 0;
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
}
}
:global {
.ant-menu-inline {
border-right: none;
}
}
}
.switchTheme {
width: 100%;
height: 48px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
overflow: hidden;
transition: all 0.3s;
span {
white-space: nowrap;
overflow: hidden;
font-size: 12px;
}
:global {
.anticon {
min-width: 14px;
margin-right: 4px;
font-size: 14px;
}
}
}
@keyframes fadeLeftIn {
0% {
transform: translateX(5px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
+6
View File
@@ -0,0 +1,6 @@
import Header from './Header'
import Menu from './Menu'
import Bread from './Bread'
import Sider from './Sider'
export { Header, Menu, Bread, Sider }
@@ -1,13 +1,9 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import styles from './index.less'
import styles from './Loader.less'
interface LoaderProps {
spinning?: boolean
fullScreen?: boolean
}
const Loader: React.FC<LoaderProps> = ({ spinning = false, fullScreen }) => {
const Loader = ({ spinning = false, fullScreen }) => {
return (
<div
className={classNames(styles.loader, {
@@ -23,4 +19,9 @@ const Loader: React.FC<LoaderProps> = ({ spinning = false, fullScreen }) => {
)
}
Loader.propTypes = {
spinning: PropTypes.bool,
fullScreen: PropTypes.bool,
}
export default Loader
@@ -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);
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Loader",
"version": "0.0.0",
"private": true,
"main": "./Loader.js"
}
+33
View File
@@ -0,0 +1,33 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Loader from '../Loader'
import styles from './Page.less'
export default class Page extends Component {
render() {
const { className, children, loading = false, inner = false } = this.props
const loadingStyle = {
height: 'calc(100vh - 184px)',
overflow: 'hidden',
}
return (
<div
className={classnames(className, {
[styles.contentInner]: inner,
})}
style={loading ? loadingStyle : null}
>
{loading ? <Loader spinning /> : ''}
{children}
</div>
)
}
}
Page.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
loading: PropTypes.bool,
inner: PropTypes.bool,
}
@@ -1,4 +1,4 @@
@import '~@/themes/vars.less';
@import '~themes/vars.less';
.contentInner {
background: #fff;
-32
View File
@@ -1,32 +0,0 @@
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
@@ -0,0 +1,6 @@
{
"name": "Page",
"version": "0.0.0",
"private": true,
"main": "./Page.js"
}
+11
View File
@@ -0,0 +1,11 @@
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
@@ -1,7 +0,0 @@
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
@@ -1 +0,0 @@
export const LOCALE_LANGUAGE = 'LOCALE_LANGUAGE'
-33
View File
@@ -1,33 +0,0 @@
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
@@ -1,8 +0,0 @@
import menus from './menus'
import menuIcon from './menus/icon'
export * from './default'
export * from './languages'
export * from './constants'
export { menus, menuIcon }
-32
View File
@@ -1,32 +0,0 @@
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',
}
-22
View File
@@ -1,22 +0,0 @@
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
@@ -1,8 +0,0 @@
import { SettingDrawerProps } from '@ant-design/pro-layout'
const defaultSettings: SettingDrawerProps['settings'] = {
navTheme: 'light',
primaryColor: '#006EFF',
}
export default defaultSettings
-41
View File
@@ -1,41 +0,0 @@
import puppeteer from 'puppeteer'
describe('Login', () => {
let browser
let page
beforeAll(async () => {
browser = await puppeteer.launch({ args: ['--no-sandbox'] })
})
beforeEach(async () => {
page = await browser.newPage()
await page.goto('http://localhost:8000/en/login', {
waitUntil: 'networkidle2',
})
})
afterEach(() => page.close())
it('should login with failure', async () => {
await page.waitFor((selector) => !!document.querySelector('#username'), {
timeout: 3000,
})
await page.type('#username', 'wrong_user')
await page.type('#password', 'wrong_password')
await page.click('button[type="button"]')
await page.waitForSelector('.anticon-close-circle') // should display error
})
it('should login successfully', async () => {
await page.waitForSelector('#username', { timeout: 3000 })
await page.type('#username', 'admin')
await page.type('#password', 'admin')
await page.click('button[type="button"]')
await page.waitForSelector('.ant-layout-footer')
const text = await page.evaluate(() => document.body.innerHTML)
expect(text).toContain('Ant Design Admin')
})
afterAll(() => browser.close())
})
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
const puppeteer = require('puppeteer-core');
(async () => {
// Use CHROME_PATH env var if provided, otherwise default macOS Chrome path
const executablePath = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const url = process.env.E2E_URL || 'http://localhost:7000';
try {
const browser = await puppeteer.launch({
executablePath,
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const title = await page.title();
console.log('E2E: page title =>', title);
await browser.close();
if (!/antd-admin/i.test(title)) process.exit(1);
process.exit(0);
} catch (err) {
console.error('E2E error:', err);
process.exit(1);
}
})();
-16
View File
@@ -1,16 +0,0 @@
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
@@ -1,8 +0,0 @@
import { useContext } from 'react'
import { ConfigContext } from '@/utils/context'
const useConfig = () => {
return useContext(ConfigContext)
}
export default useConfig
-47
View File
@@ -1,47 +0,0 @@
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
@@ -1,29 +0,0 @@
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
@@ -1,56 +0,0 @@
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
@@ -1,30 +0,0 @@
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
@@ -0,0 +1,55 @@
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
+12 -12
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;
top: 0;
right: 0;
left: 0;
z-index: 2048;
top: 0;
left: 0;
right: 0;
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;
transform: rotate(3deg) translate(0, -4px);
opacity: 1;
transform: rotate(3deg) translate(0, -4px);
}
.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 {
position: relative;
overflow: hidden;
position: relative;
#nprogress {
.bar,
-21
View File
@@ -1,21 +0,0 @@
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
+166
View File
@@ -0,0 +1,166 @@
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 { Drawer, FloatButton, Layout } from 'antd';
import { enquireScreen, unenquireScreen } from 'enquire-js'
import { pathToRegexp } from '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
mask={{ closable: true }}
closable={false}
onClose={onCollapseChange.bind(this, !collapsed)}
open={!collapsed}
placement="left"
width={200}
rootStyle={{
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>
<FloatButton.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
@@ -1,9 +1,5 @@
@import '~@/themes/vars.less';
:global {
#root {
height: 100%;
}
}
@import '~themes/vars.less';
.backTop {
right: 50px;
@@ -1,69 +0,0 @@
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
-77
View File
@@ -1,77 +0,0 @@
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
@@ -0,0 +1,3 @@
const PublicLayout = ({ children }) => children
export default PublicLayout
-8
View File
@@ -1,8 +0,0 @@
import React from 'react'
import { Outlet } from 'umi'
const PublicLayout: React.FC = (props) => {
return <><Outlet /></>
}
export default PublicLayout
+94
View File
@@ -0,0 +1,94 @@
import React, { Component } from 'react'
import { withRouter } from 'umi'
import { ConfigProvider } from 'antd'
import { i18n } from "@lingui/core"
import { I18nProvider } from '@lingui/react'
import { getLocale } from 'utils'
import config from 'utils/config'
import { zh, en, pt } from 'make-plural/plurals'
import enMessages from '../locales/en/messages.json'
import zhMessages from '../locales/zh/messages.json'
import ptMessages from '../locales/pt-br/messages.json'
import zhCN from 'antd/locale/zh_CN'
import enUS from 'antd/locale/en_US'
import ptBR from 'antd/locale/pt_BR'
import BaseLayout from './BaseLayout'
i18n.loadLocaleData({
en: { plurals: en },
zh: { plurals: zh },
'pt-br': { plurals: pt }
})
// Activate the default language synchronously so I18nProvider doesn't render `null` in dev.
// Avoid activating the user-selected language here because its messages may not be loaded yet.
const configuredDefault = (config && config.i18n && config.i18n.defaultLanguage) || 'en'
const _catalogs = {
en: enMessages,
zh: zhMessages,
'pt-br': ptMessages,
}
try {
const initialCatalog = _catalogs[configuredDefault]
if (initialCatalog) {
i18n.load(configuredDefault, initialCatalog)
}
i18n.activate(configuredDefault)
} catch (e) {
// ignore activation errors during bootstrap
}
// antd
const languages = {
zh: zhCN,
en: enUS,
'pt-br': ptBR
}
// remove unused defaultLanguage
@withRouter
class Layout extends Component {
state = {
}
componentDidMount() {
let language = getLocale()
if (!languages[language]) language = configuredDefault
this.loadCatalog(language).catch(() => {})
}
loadCatalog = async (lan) => {
const language = lan || i18n.defaultLanguage
try {
const catalogModule = await import(`../locales/${language}/messages.json`)
const catalog = catalogModule && catalogModule.default ? catalogModule.default : catalogModule
i18n.load(language, catalog)
i18n.activate(language)
} catch (err) {
// If loading fails, fall back to default language and log a warning.
// eslint-disable-next-line no-console
console.warn(`Failed to load messages for locale "${language}":`, err)
}
}
render() {
const { children } = this.props
let language = getLocale()
if (!languages[language]) language = configuredDefault
return (
<ConfigProvider locale={languages[language]}>
<I18nProvider i18n={i18n}>
<BaseLayout>{children}</BaseLayout>
</I18nProvider>
</ConfigProvider>
)
}
}
export default Layout
-22
View File
@@ -1,22 +0,0 @@
: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
@@ -1,53 +0,0 @@
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
+1 -1
View File
@@ -1,4 +1,5 @@
{
"Password": "Password",
"/dashboard": "/dashboard",
"Add Param": "Add Param",
"Address": "Address",
@@ -26,7 +27,6 @@
"Not Found": "Not Found",
"Operation": "Operation",
"Params": "Params",
"Password": "Password",
"Phone": "Phone",
"Pick an address": "Pick an address",
"Please pick an address": "Please pick an address",
-26
View File
@@ -1,26 +0,0 @@
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"
+1 -1
View File
@@ -1,4 +1,5 @@
{
"Password": "Senha",
"/dashboard": "/dashboard",
"Add Param": "Add Parametro",
"Address": "Endereço",
@@ -26,7 +27,6 @@
"Not Found": "Não Encontrado",
"Operation": "Operation",
"Params": "Parametros",
"Password": "Senha",
"Phone": "Fone",
"Pick an address": "Escolha um endereço",
"Please pick an address": "Por favor, escolha um endereço",
-20
View File
@@ -1,20 +0,0 @@
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 ""
+1 -1
View File
@@ -1,4 +1,5 @@
{
"Password": "密码",
"/dashboard": "/zh/dashboard",
"Add Param": "添加参数",
"Address": "地址",
@@ -26,7 +27,6 @@
"Not Found": "未找到",
"Operation": "操作",
"Params": "参数",
"Password": "密码",
"Phone": "电话",
"Pick an address": "选择地址",
"Please pick an address": "选择地址",
-26
View File
@@ -1,26 +0,0 @@
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
@@ -0,0 +1,160 @@
import { history } from 'umi'
import { stringify } from 'qs'
import store from 'store'
import { pathToRegexp } from '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',
})
}
}
const appModel = {
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.abort(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 = []
},
},
}
export default appModel
+10 -5
View File
@@ -1,10 +1,15 @@
import React from 'react'
import { FrownOutlined } from '@ant-design/icons'
import { Page } from 'components'
import styles from './404.less'
const NotFoundPage: React.FC = () => (
<div className={styles.error}>
<h1>404 Not Found</h1>
</div>
const Error = () => (
<Page inner>
<div className={styles.error}>
<FrownOutlined />
<h1>404 Not Found</h1>
</div>
</Page>
)
export default NotFoundPage
export default Error
@@ -0,0 +1,161 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
import * as 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
},
emphasis: {
label: {
show: true,
formatter(param) {
return param.data[3]
},
position: 'top',
},
},
itemStyle: {
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
},
emphasis: {
label: {
show: true,
formatter(param) {
return param.data[3]
},
position: 'top',
},
},
itemStyle: {
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
@@ -0,0 +1,193 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
import * as 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
@@ -0,0 +1,122 @@
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: {
formatter: '{b}预期',
},
emphasis: {
label: {
position: 'inside',
formatter: '{b}预期: {c}%',
},
},
labelLine: {
show: false,
},
itemStyle: {
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: {
position: 'inside',
formatter: '{c}%',
color: '#fff',
},
emphasis: {
label: {
position: 'inside',
formatter: '{b}实际: {c}%',
},
},
itemStyle: {
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
@@ -0,0 +1,106 @@
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: 7000 },
{ name: '管理(Administration)', max: 16000 },
{ name: '信息技术(Information Techology)', max: 30000 },
{ name: '客服(Customer Support)', max: 38000 },
{ name: '研发(Development)', max: 52000 },
{ name: '市场(Marketing)', max: 25000 },
],
splitNumber: 5,
alignTicks: false,
},
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
@@ -0,0 +1,104 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
const ChartWithEventComponent = () => {
const onChartReady = echart => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log('echart is ready', echart)
}
}
const onChartLegendselectchanged = (param, echart) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(param, echart)
}
}
const onChartClick = (param, echart) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
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: {
itemStyle: {
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
@@ -0,0 +1,214 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
class DynamicChartComponent extends React.Component {
constructor(props) {
super(props)
this.echartsRef = React.createRef()
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,
alignTicks: false,
boundaryGap: [0.2, 0.2],
},
{
type: 'value',
scale: true,
name: '预购量',
max: 1200,
min: 0,
alignTicks: false,
boundaryGap: [0.2, 0.2],
},
],
series: [
{
name: '预购队列',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
borderRadius: 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={this.echartsRef} \n" +
' option={this.state.option} />\n'
return (
<div className="examples">
<div className="parent">
<label> use React state to render dynamic chart</label>
<ReactEcharts ref={this.echartsRef} 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
@@ -0,0 +1,46 @@
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'
// removed MapChartComponent and 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 />
// map and airport chart types removed
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
@@ -0,0 +1,76 @@
import React from 'react'
import ReactEcharts from 'echarts-for-react'
import * as 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
+354
View File
@@ -0,0 +1,354 @@
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: {
// 坐标轴小标记
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
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: {
// 坐标轴小标记
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
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: {
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: {
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