This commit is contained in:
medcl 2020-03-16 20:56:25 +08:00
parent a360f6a627
commit 7ed52851e5
364 changed files with 28050 additions and 0 deletions

33
.autod.conf.js Normal file
View File

@ -0,0 +1,33 @@
'use strict';
module.exports = {
write: true,
prefix: '^',
plugin: 'autod-egg',
test: [
'test',
'benchmark',
'script',
'.roadhogrc.mock.js',
],
dep: [
'egg',
'egg-scripts',
],
devdep: [
'egg-ci',
'egg-bin',
'egg-mock',
'autod',
'autod-egg',
'eslint',
'eslint-config-egg',
'webstorm-disable-index',
],
exclude: [
'./test/fixtures',
'./dist',
'**/*.test.js',
'**/*.e2e.js',
],
};

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules/
.git/

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
coverage
node_modules
app/public

3
.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "eslint-config-egg"
}

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
logs/
npm-debug.log
yarn-error.log
node_modules
package-lock.json
yarn.lock
coverage/
.idea/
run/
.DS_Store
*.sw*
*.un~
app/public
config/manifest.json
app/web/.temp
.umi
docker/.node_modules/*

24
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,24 @@
pipeline {
agent any
environment {
CI = 'true'
}
stages {
stage('Build') {
steps {
sh 'cnpm install'
}
}
stage('Test') {
steps {
sh '/bin/true'
}
}
stage('Deliver') {
steps {
sh '/bin/true'
}
}
}
}

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# 极限日志分析中心
INFINI Logging Center
## 开发说明
### 开发环境准备
确保已经安装好`nodejs`(版本大于等于 8.5.0)环境:
```sh
node -v
npm -v
```
在国内,你可以安装 `cnpm` 获得更快速、更安全的包管理体验。使用如下命令安装:
```sh
npm install -g cnpm --registry=https://registry.npm.taobao.org
```
### 下载项目依赖包
```
cnpm install
```
### 启动开发模式
```sh
cnpm run dev
```
在浏览器中访问:[http://localhost:8000](http://localhost:8000)
### 构建和部署
```sh
cnpm run build
```
执行该命令后会生成最终的 HTML、CSS 和 JS 到 `dist` 目录下。它们是浏览器可以直接识别并运行的代码,这样你就可以将它们部署到你想要的服务器上了。
### 新增项目依赖包
```
cnpm install --save md5
```

23
app/controller/home.js Normal file
View File

@ -0,0 +1,23 @@
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
await this.ctx.render('index.html');
}
async proxy() {
const ctx = this.ctx;
// use roadhog mock api first
const url = this.app.config.assets.url + ctx.path + '?' + ctx.querystring;
const res = await this.ctx.curl(url, {
method: this.ctx.method,
});
ctx.body = res.data;
ctx.status = res.status;
}
}
module.exports = HomeController;

10
app/router.js Normal file
View File

@ -0,0 +1,10 @@
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.all('/api/*', controller.home.proxy);
router.get('*', controller.home.index);
};

27
app/view/index.html Normal file
View File

@ -0,0 +1,27 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="format-detection" content="email=no"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<title></title>
{{ helper.assets.getStyle('umi.css') | safe }}
</head>
<body>
<div id="root"></div>
<script>
window.routerBase = '/';
window.resourceBaseUrl = '{{ helper.assets.resourceBase }}';
</script>
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.data-set-0.9.6/dist/data-set.min.js"></script>
{% if ctx.app.config.env === 'local' -%}
{{ helper.assets.getScript('umi.dll.js') | safe }}
{%- endif %}
{{ helper.assets.getScript('umi.js') | safe }}
</body>
</html>

32
app/web/.eslintrc.js Normal file
View File

@ -0,0 +1,32 @@
module.exports = {
parser: 'babel-eslint',
extends: ['airbnb', 'prettier', 'plugin:compat/recommended'],
env: {
browser: true,
node: true,
es6: true,
mocha: true,
jest: true,
jasmine: true,
},
globals: {
APP_TYPE: true,
},
rules: {
'react/jsx-filename-extension': [1, { extensions: ['.js'] }],
'react/jsx-wrap-multilines': 0,
'react/prop-types': 0,
'react/forbid-prop-types': 0,
'react/jsx-one-expression-per-line': 0,
'import/no-unresolved': [2, { ignore: ['^@/', '^umi/'] }],
'import/no-extraneous-dependencies': [2, { optionalDependencies: true }],
'jsx-a11y/no-noninteractive-element-interactions': 0,
'jsx-a11y/click-events-have-key-events': 0,
'jsx-a11y/no-static-element-interactions': 0,
'jsx-a11y/anchor-is-valid': 0,
'linebreak-style': 0,
},
settings: {
polyfills: ['fetch', 'promises', 'url'],
},
};

111
app/web/config/config.js Normal file
View File

@ -0,0 +1,111 @@
// https://umijs.org/config/
import os from 'os';
import pageRoutes from './router.config';
import webpackPlugin from './plugin.config';
import defaultSettings from '../src/defaultSettings';
export default {
// add for transfer to umi
plugins: [
[
'umi-plugin-react',
{
antd: true,
dva: {
hmr: true,
},
targets: {
ie: 11,
},
locale: {
enable: true, // default false
default: 'zh-CN', // default zh-CN
baseNavigator: true, // default true, when it is true, will use `navigator.language` overwrite default
},
dynamicImport: {
loadingComponent: './components/PageLoading/index',
},
...(!process.env.TEST && os.platform() === 'darwin'
? {
dll: {
include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
exclude: ['@babel/runtime'],
},
hardSource: true,
}
: {}),
},
],
[
'umi-plugin-ga',
{
code: 'UA-72788897-6',
judge: () => process.env.APP_TYPE === 'site',
},
],
],
targets: {
ie: 11,
},
define: {
APP_TYPE: process.env.APP_TYPE || '',
},
// 路由配置
routes: pageRoutes,
// Theme for antd
// https://ant.design/docs/react/customize-theme-cn
theme: {
'primary-color': defaultSettings.primaryColor,
},
externals: {
'@antv/data-set': 'DataSet',
},
// proxy: {
// '/server/api/': {
// target: 'https://preview.pro.ant.design/',
// changeOrigin: true,
// pathRewrite: { '^/server': '' },
// },
// },
ignoreMomentLocale: true,
lessLoaderOptions: {
javascriptEnabled: true,
},
disableRedirectHoist: true,
cssLoaderOptions: {
modules: true,
getLocalIdent: (context, localIdentName, localName) => {
if (
context.resourcePath.includes('node_modules') ||
context.resourcePath.includes('ant.design.pro.less') ||
context.resourcePath.includes('global.less')
) {
return localName;
}
const match = context.resourcePath.match(/src(.*)/);
if (match && match[1]) {
const antdProPath = match[1].replace('.less', '');
const arr = antdProPath
.split('/')
.map(a => a.replace(/([A-Z])/g, '-$1'))
.map(a => a.toLowerCase());
return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
}
return localName;
},
},
chainWebpack: webpackPlugin,
cssnano: {
mergeRules: false,
},
// extra configuration for egg
runtimePublicPath: true,
hash: true,
outputPath: '../public',
manifest: {
fileName: '../../config/manifest.json',
publicPath: '',
},
};

View File

@ -0,0 +1,28 @@
// Change theme plugin
import MergeLessPlugin from 'antd-pro-merge-less';
import AntDesignThemePlugin from 'antd-theme-webpack-plugin';
import path from 'path';
export default config => {
// 将所有 less 合并为一个供 themePlugin使用
const outFile = path.join(__dirname, '../.temp/ant-design-pro.less');
const stylesDir = path.join(__dirname, '../src/');
config.plugin('merge-less').use(MergeLessPlugin, [
{
stylesDir,
outFile,
},
]);
config.plugin('ant-design-theme').use(AntDesignThemePlugin, [
{
antDir: require.resolve('antd').replace('lib/index.js', ''),
stylesDir,
varFile: require.resolve('antd/lib/style/themes/default.less'),
mainLessFile: outFile, // themeVariables: ['@primary-color'],
indexFileName: 'index.html',
},
]);
};

View File

@ -0,0 +1,264 @@
export default [
// user
{
path: '/user',
component: '../layouts/UserLayout',
routes: [
{ path: '/user', redirect: '/user/login' },
{ path: '/user/login', component: './User/Login' },
{ path: '/user/register', component: './User/Register' },
{ path: '/user/register-result', component: './User/RegisterResult' },
],
},
// app
{
path: '/',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'],
routes: [
// dashboard
{ path: '/', redirect: '/dashboard/analysis' },
{
path: '/dashboard',
name: 'dashboard',
icon: 'dashboard',
routes: [
{
path: '/dashboard/analysis',
name: 'analysis',
component: './Dashboard/Analysis',
},
{
path: '/dashboard/monitor',
name: 'monitor',
component: './Dashboard/Monitor',
},
{
path: '/dashboard/workplace',
name: 'workplace',
component: './Dashboard/Workplace',
},
],
},
// forms
{
path: '/form',
icon: 'form',
name: 'form',
routes: [
{
path: '/form/basic-form',
name: 'basicform',
component: './Forms/BasicForm',
},
{
path: '/form/step-form',
name: 'stepform',
component: './Forms/StepForm',
hideChildrenInMenu: true,
routes: [
{
path: '/form/step-form',
name: 'stepform',
redirect: '/form/step-form/info',
},
{
path: '/form/step-form/info',
name: 'info',
component: './Forms/StepForm/Step1',
},
{
path: '/form/step-form/confirm',
name: 'confirm',
component: './Forms/StepForm/Step2',
},
{
path: '/form/step-form/result',
name: 'result',
component: './Forms/StepForm/Step3',
},
],
},
{
path: '/form/advanced-form',
name: 'advancedform',
authority: ['admin'],
component: './Forms/AdvancedForm',
},
],
},
// list
{
path: '/list',
icon: 'table',
name: 'list',
routes: [
{
path: '/list/table-list',
name: 'searchtable',
component: './List/TableList',
},
{
path: '/list/basic-list',
name: 'basiclist',
component: './List/BasicList',
},
{
path: '/list/card-list',
name: 'cardlist',
component: './List/CardList',
},
{
path: '/list/search',
name: 'searchlist',
component: './List/List',
routes: [
{
path: '/list/search',
redirect: '/list/search/articles',
},
{
path: '/list/search/articles',
name: 'articles',
component: './List/Articles',
},
{
path: '/list/search/projects',
name: 'projects',
component: './List/Projects',
},
{
path: '/list/search/applications',
name: 'applications',
component: './List/Applications',
},
],
},
],
},
{
path: '/profile',
name: 'profile',
icon: 'profile',
routes: [
// profile
{
path: '/profile/basic',
name: 'basic',
component: './Profile/BasicProfile',
},
{
path: '/profile/advanced',
name: 'advanced',
authority: ['admin'],
component: './Profile/AdvancedProfile',
},
],
},
{
name: 'result',
icon: 'check-circle-o',
path: '/result',
routes: [
// result
{
path: '/result/success',
name: 'success',
component: './Result/Success',
},
{ path: '/result/fail', name: 'fail', component: './Result/Error' },
],
},
{
name: 'exception',
icon: 'warning',
path: '/exception',
routes: [
// exception
{
path: '/exception/403',
name: 'not-permission',
component: './Exception/403',
},
{
path: '/exception/404',
name: 'not-find',
component: './Exception/404',
},
{
path: '/exception/500',
name: 'server-error',
component: './Exception/500',
},
{
path: '/exception/trigger',
name: 'trigger',
hideInMenu: true,
component: './Exception/TriggerException',
},
],
},
{
name: 'account',
icon: 'user',
path: '/account',
routes: [
{
path: '/account/center',
name: 'center',
component: './Account/Center/Center',
routes: [
{
path: '/account/center',
redirect: '/account/center/articles',
},
{
path: '/account/center/articles',
component: './Account/Center/Articles',
},
{
path: '/account/center/applications',
component: './Account/Center/Applications',
},
{
path: '/account/center/projects',
component: './Account/Center/Projects',
},
],
},
{
path: '/account/settings',
name: 'settings',
component: './Account/Settings/Info',
routes: [
{
path: '/account/settings',
redirect: '/account/settings/base',
},
{
path: '/account/settings/base',
component: './Account/Settings/BaseView',
},
{
path: '/account/settings/security',
component: './Account/Settings/SecurityView',
},
{
path: '/account/settings/binding',
component: './Account/Settings/BindingView',
},
{
path: '/account/settings/notification',
component: './Account/Settings/NotificationView',
},
],
},
],
},
{
component: '404',
},
],
},
];

336
app/web/mock/api.js Normal file
View File

@ -0,0 +1,336 @@
import mockjs from 'mockjs';
const titles = [
'Alipay',
'Angular',
'Ant Design',
'Ant Design Pro',
'Bootstrap',
'React',
'Vue',
'Webpack',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const avatars2 = [
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png',
'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png',
'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png',
'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png',
'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png',
];
const covers = [
'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
];
const desc = [
'那是一种内在的东西, 他们到达不了,也无法触及的',
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
'生命就像一盒巧克力,结果往往出人意料',
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
'那时候我只会想自己想要什么,从不想自己拥有什么',
];
const user = [
'付小小',
'曲丽丽',
'林东东',
'周星星',
'吴加好',
'朱偏右',
'鱼酱',
'乐哥',
'谭小仪',
'仲尼',
];
function fakeList(count) {
const list = [];
for (let i = 0; i < count; i += 1) {
list.push({
id: `fake-list-${i}`,
owner: user[i % 10],
title: titles[i % 8],
avatar: avatars[i % 8],
cover: parseInt(i / 4, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
status: ['active', 'exception', 'normal'][i % 3],
percent: Math.ceil(Math.random() * 50) + 50,
logo: avatars[i % 8],
href: 'https://ant.design',
updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
subDescription: desc[i % 5],
description:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
activeUser: Math.ceil(Math.random() * 100000) + 100000,
newUser: Math.ceil(Math.random() * 1000) + 1000,
star: Math.ceil(Math.random() * 100) + 100,
like: Math.ceil(Math.random() * 100) + 100,
message: Math.ceil(Math.random() * 10) + 10,
content:
'段落示意:蚂蚁金服设计平台 ant.design用最小的工作量无缝接入蚂蚁金服生态提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design用最小的工作量无缝接入蚂蚁金服生态提供跨越设计与开发的体验解决方案。',
members: [
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
name: '曲丽丽',
id: 'member1',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
name: '王昭君',
id: 'member2',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
name: '董娜娜',
id: 'member3',
},
],
});
}
return list;
}
let sourceData;
function getFakeList(req, res) {
const params = req.query;
const count = params.count * 1 || 20;
const result = fakeList(count);
sourceData = result;
return res.json(result);
}
function postFakeList(req, res) {
const { /* url = '', */ body } = req;
// const params = getUrlParams(url);
const { method, id } = body;
// const count = (params.count * 1) || 20;
let result = sourceData;
switch (method) {
case 'delete':
result = result.filter(item => item.id !== id);
break;
case 'update':
result.forEach((item, i) => {
if (item.id === id) {
result[i] = Object.assign(item, body);
}
});
break;
case 'post':
result.unshift({
body,
id: `fake-list-${result.length}`,
createdAt: new Date().getTime(),
});
break;
default:
break;
}
return res.json(result);
}
const getNotice = [
{
id: 'xxx1',
title: titles[0],
logo: avatars[0],
description: '那是一种内在的东西,他们到达不了,也无法触及的',
updatedAt: new Date(),
member: '科学搬砖组',
href: '',
memberLink: '',
},
{
id: 'xxx2',
title: titles[1],
logo: avatars[1],
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
updatedAt: new Date('2017-07-24'),
member: '全组都是吴彦祖',
href: '',
memberLink: '',
},
{
id: 'xxx3',
title: titles[2],
logo: avatars[2],
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
updatedAt: new Date(),
member: '中二少女团',
href: '',
memberLink: '',
},
{
id: 'xxx4',
title: titles[3],
logo: avatars[3],
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
updatedAt: new Date('2017-07-23'),
member: '程序员日常',
href: '',
memberLink: '',
},
{
id: 'xxx5',
title: titles[4],
logo: avatars[4],
description: '凛冬将至',
updatedAt: new Date('2017-07-23'),
member: '高逼格设计天团',
href: '',
memberLink: '',
},
{
id: 'xxx6',
title: titles[5],
logo: avatars[5],
description: '生命就像一盒巧克力,结果往往出人意料',
updatedAt: new Date('2017-07-23'),
member: '骗你来学计算机',
href: '',
memberLink: '',
},
];
const getActivities = [
{
id: 'trend-1',
updatedAt: new Date(),
user: {
name: '曲丽丽',
avatar: avatars2[0],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-2',
updatedAt: new Date(),
user: {
name: '付小小',
avatar: avatars2[1],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-3',
updatedAt: new Date(),
user: {
name: '林东东',
avatar: avatars2[2],
},
group: {
name: '中二少女团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-4',
updatedAt: new Date(),
user: {
name: '周星星',
avatar: avatars2[4],
},
project: {
name: '5 月日常迭代',
link: 'http://github.com/',
},
template: '将 @{project} 更新至已发布状态',
},
{
id: 'trend-5',
updatedAt: new Date(),
user: {
name: '朱偏右',
avatar: avatars2[3],
},
project: {
name: '工程效能',
link: 'http://github.com/',
},
comment: {
name: '留言',
link: 'http://github.com/',
},
template: '在 @{project} 发布了 @{comment}',
},
{
id: 'trend-6',
updatedAt: new Date(),
user: {
name: '乐哥',
avatar: avatars2[5],
},
group: {
name: '程序员日常',
link: 'http://github.com/',
},
project: {
name: '品牌迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
];
function getFakeCaptcha(req, res) {
return res.json('captcha-xxx');
}
export default {
'GET /api/project/notice': getNotice,
'GET /api/activities': getActivities,
'POST /api/forms': (req, res) => {
res.send({ message: 'Ok' });
},
'GET /api/tags': mockjs.mock({
'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }],
}),
'GET /api/fake_list': getFakeList,
'POST /api/fake_list': postFakeList,
'GET /api/captcha': getFakeCaptcha,
};

196
app/web/mock/chart.js Normal file
View File

@ -0,0 +1,196 @@
import moment from 'moment';
// mock data
const visitData = [];
const beginDay = new Date().getTime();
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY[i],
});
}
const visitData2 = [];
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
for (let i = 0; i < fakeY2.length; i += 1) {
visitData2.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY2[i],
});
}
const salesData = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
const searchData = [];
for (let i = 0; i < 50; i += 1) {
searchData.push({
index: i + 1,
keyword: `搜索关键词-${i}`,
count: Math.floor(Math.random() * 1000),
range: Math.floor(Math.random() * 100),
status: Math.floor((Math.random() * 10) % 2),
});
}
const salesTypeData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
const salesTypeDataOnline = [
{
x: '家用电器',
y: 244,
},
{
x: '食用酒水',
y: 321,
},
{
x: '个护健康',
y: 311,
},
{
x: '服饰箱包',
y: 41,
},
{
x: '母婴产品',
y: 121,
},
{
x: '其他',
y: 111,
},
];
const salesTypeDataOffline = [
{
x: '家用电器',
y: 99,
},
{
x: '食用酒水',
y: 188,
},
{
x: '个护健康',
y: 344,
},
{
x: '服饰箱包',
y: 255,
},
{
x: '其他',
y: 65,
},
];
const offlineData = [];
for (let i = 0; i < 10; i += 1) {
offlineData.push({
name: `Stores ${i}`,
cvr: Math.ceil(Math.random() * 9) / 10,
});
}
const offlineChartData = [];
for (let i = 0; i < 20; i += 1) {
offlineChartData.push({
x: new Date().getTime() + 1000 * 60 * 30 * i,
y1: Math.floor(Math.random() * 100) + 10,
y2: Math.floor(Math.random() * 100) + 10,
});
}
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
const radarData = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach(item => {
Object.keys(item).forEach(key => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key],
value: item[key],
});
}
});
});
const getFakeChartData = {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
};
export default {
'GET /api/fake_chart_data': getFakeChartData,
};

View File

@ -0,0 +1,15 @@
import city from './geographic/city.json';
import province from './geographic/province.json';
function getProvince(req, res) {
return res.json(province);
}
function getCity(req, res) {
return res.json(city[req.params.province]);
}
export default {
'GET /api/geographic/province': getProvince,
'GET /api/geographic/city/:province': getCity,
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,138 @@
[
{
"name": "北京市",
"id": "110000"
},
{
"name": "天津市",
"id": "120000"
},
{
"name": "河北省",
"id": "130000"
},
{
"name": "山西省",
"id": "140000"
},
{
"name": "内蒙古自治区",
"id": "150000"
},
{
"name": "辽宁省",
"id": "210000"
},
{
"name": "吉林省",
"id": "220000"
},
{
"name": "黑龙江省",
"id": "230000"
},
{
"name": "上海市",
"id": "310000"
},
{
"name": "江苏省",
"id": "320000"
},
{
"name": "浙江省",
"id": "330000"
},
{
"name": "安徽省",
"id": "340000"
},
{
"name": "福建省",
"id": "350000"
},
{
"name": "江西省",
"id": "360000"
},
{
"name": "山东省",
"id": "370000"
},
{
"name": "河南省",
"id": "410000"
},
{
"name": "湖北省",
"id": "420000"
},
{
"name": "湖南省",
"id": "430000"
},
{
"name": "广东省",
"id": "440000"
},
{
"name": "广西壮族自治区",
"id": "450000"
},
{
"name": "海南省",
"id": "460000"
},
{
"name": "重庆市",
"id": "500000"
},
{
"name": "四川省",
"id": "510000"
},
{
"name": "贵州省",
"id": "520000"
},
{
"name": "云南省",
"id": "530000"
},
{
"name": "西藏自治区",
"id": "540000"
},
{
"name": "陕西省",
"id": "610000"
},
{
"name": "甘肃省",
"id": "620000"
},
{
"name": "青海省",
"id": "630000"
},
{
"name": "宁夏回族自治区",
"id": "640000"
},
{
"name": "新疆维吾尔自治区",
"id": "650000"
},
{
"name": "台湾省",
"id": "710000"
},
{
"name": "香港特别行政区",
"id": "810000"
},
{
"name": "澳门特别行政区",
"id": "820000"
}
]

99
app/web/mock/notices.js Normal file
View File

@ -0,0 +1,99 @@
const getNotices = (req, res) =>
res.json([
{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: 'notification',
},
{
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: 'notification',
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: 'notification',
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: 'message',
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
},
{
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
type: 'event',
},
{
id: '000000010',
title: '第三方紧急代码变更',
description: '冠霖提交于 2017-01-06需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: 'event',
},
{
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
type: 'event',
},
{
id: '000000012',
title: 'ABCD 版本发布',
description: '冠霖提交于 2017-01-06需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: 'event',
},
]);
export default {
'GET /api/notices': getNotices,
};

158
app/web/mock/profile.js Normal file
View File

@ -0,0 +1,158 @@
const basicGoods = [
{
id: '1234561',
name: '矿泉水 550ml',
barcode: '12421432143214321',
price: '2.00',
num: '1',
amount: '2.00',
},
{
id: '1234562',
name: '凉茶 300ml',
barcode: '12421432143214322',
price: '3.00',
num: '2',
amount: '6.00',
},
{
id: '1234563',
name: '好吃的薯片',
barcode: '12421432143214323',
price: '7.00',
num: '4',
amount: '28.00',
},
{
id: '1234564',
name: '特别好吃的蛋卷',
barcode: '12421432143214324',
price: '8.50',
num: '3',
amount: '25.50',
},
];
const basicProgress = [
{
key: '1',
time: '2017-10-01 14:10',
rate: '联系客户',
status: 'processing',
operator: '取货员 ID1234',
cost: '5mins',
},
{
key: '2',
time: '2017-10-01 14:05',
rate: '取货员出发',
status: 'success',
operator: '取货员 ID1234',
cost: '1h',
},
{
key: '3',
time: '2017-10-01 13:05',
rate: '取货员接单',
status: 'success',
operator: '取货员 ID1234',
cost: '5mins',
},
{
key: '4',
time: '2017-10-01 13:00',
rate: '申请审批通过',
status: 'success',
operator: '系统',
cost: '1h',
},
{
key: '5',
time: '2017-10-01 12:00',
rate: '发起退货申请',
status: 'success',
operator: '用户',
cost: '5mins',
},
];
const advancedOperation1 = [
{
key: 'op1',
type: '订购关系生效',
name: '曲丽丽',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
{
key: 'op2',
type: '财务复审',
name: '付小小',
status: 'reject',
updatedAt: '2017-10-03 19:23:12',
memo: '不通过原因',
},
{
key: 'op3',
type: '部门初审',
name: '周毛毛',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
{
key: 'op4',
type: '提交订单',
name: '林东东',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '很棒',
},
{
key: 'op5',
type: '创建订单',
name: '汗牙牙',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
const advancedOperation2 = [
{
key: 'op1',
type: '订购关系生效',
name: '曲丽丽',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
const advancedOperation3 = [
{
key: 'op1',
type: '创建订单',
name: '汗牙牙',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
const getProfileBasicData = {
basicGoods,
basicProgress,
};
const getProfileAdvancedData = {
advancedOperation1,
advancedOperation2,
advancedOperation3,
};
export default {
'GET /api/profile/advanced': getProfileAdvancedData,
'GET /api/profile/basic': getProfileBasicData,
};

138
app/web/mock/rule.js Normal file
View File

@ -0,0 +1,138 @@
import { parse } from 'url';
// mock tableListDataSource
let tableListDataSource = [];
for (let i = 0; i < 46; i += 1) {
tableListDataSource.push({
key: i,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name: `TradeCode ${i}`,
title: `一个任务名称 ${i}`,
owner: '曲丽丽',
desc: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
progress: Math.ceil(Math.random() * 100),
});
}
function getRule(req, res, u) {
let url = u;
if (!url || Object.prototype.toString.call(url) !== '[object String]') {
url = req.url; // eslint-disable-line
}
const params = parse(url, true).query;
let dataSource = tableListDataSource;
if (params.sorter) {
const s = params.sorter.split('_');
dataSource = dataSource.sort((prev, next) => {
if (s[1] === 'descend') {
return next[s[0]] - prev[s[0]];
}
return prev[s[0]] - next[s[0]];
});
}
if (params.status) {
const status = params.status.split(',');
let filterDataSource = [];
status.forEach(s => {
filterDataSource = filterDataSource.concat(
dataSource.filter(data => parseInt(data.status, 10) === parseInt(s[0], 10))
);
});
dataSource = filterDataSource;
}
if (params.name) {
dataSource = dataSource.filter(data => data.name.indexOf(params.name) > -1);
}
let pageSize = 10;
if (params.pageSize) {
pageSize = params.pageSize * 1;
}
const result = {
list: dataSource,
pagination: {
total: dataSource.length,
pageSize,
current: parseInt(params.currentPage, 10) || 1,
},
};
return res.json(result);
}
function postRule(req, res, u, b) {
let url = u;
if (!url || Object.prototype.toString.call(url) !== '[object String]') {
url = req.url; // eslint-disable-line
}
const body = (b && b.body) || req.body;
const { method, name, desc, key } = body;
switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
tableListDataSource = tableListDataSource.filter(item => key.indexOf(item.key) === -1);
break;
case 'post':
const i = Math.ceil(Math.random() * 10000);
tableListDataSource.unshift({
key: i,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name: `TradeCode ${i}`,
title: `一个任务名称 ${i}`,
owner: '曲丽丽',
desc,
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 2,
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
});
break;
case 'update':
tableListDataSource = tableListDataSource.map(item => {
if (item.key === key) {
Object.assign(item, { desc, name });
return item;
}
return item;
});
break;
default:
break;
}
const result = {
list: tableListDataSource,
pagination: {
total: tableListDataSource.length,
},
};
return res.json(result);
}
export default {
'GET /api/rule': getRule,
'POST /api/rule': postRule,
};

137
app/web/mock/user.js Normal file
View File

@ -0,0 +1,137 @@
// 代码中会兼容本地 service mock 以及部署站点的静态数据
export default {
// 支持值为 Object 和 Array
'GET /api/currentUser': {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服某某某事业群某某平台部某某技术部UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notifyCount: 12,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
// GET POST 可省略
'GET /api/users': [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
],
'POST /api/login/account': (req, res) => {
const { password, userName, type } = req.body;
if (password === '888888' && userName === 'admin') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin',
});
return;
}
if (password === '123456' && userName === 'user') {
res.send({
status: 'ok',
type,
currentAuthority: 'user',
});
return;
}
res.send({
status: 'error',
type,
currentAuthority: 'guest',
});
},
'POST /api/register': (req, res) => {
res.send({ status: 'ok', currentAuthority: 'user' });
},
'GET /api/500': (req, res) => {
res.status(500).send({
timestamp: 1513932555104,
status: 500,
error: 'error',
message: 'error',
path: '/base/category/list',
});
},
'GET /api/404': (req, res) => {
res.status(404).send({
timestamp: 1513932643431,
status: 404,
error: 'Not Found',
message: 'No message available',
path: '/base/category/list/2121212',
});
},
'GET /api/403': (req, res) => {
res.status(403).send({
timestamp: 1513932555104,
status: 403,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/401': (req, res) => {
res.status(401).send({
timestamp: 1513932555104,
status: 401,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
};

1007
app/web/src/assets/logo.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -0,0 +1,93 @@
import React, { Component } from 'react';
import { MiniArea } from '../Charts';
import NumberInfo from '../NumberInfo';
import styles from './index.less';
function fixedZero(val) {
return val * 1 < 10 ? `0${val}` : val;
}
function getActiveData() {
const activeData = [];
for (let i = 0; i < 24; i += 1) {
activeData.push({
x: `${fixedZero(i)}:00`,
y: Math.floor(Math.random() * 200) + i * 50,
});
}
return activeData;
}
export default class ActiveChart extends Component {
state = {
activeData: getActiveData(),
};
componentDidMount() {
this.loopData();
}
componentWillUnmount() {
clearTimeout(this.timer);
cancelAnimationFrame(this.requestRef);
}
loopData = () => {
this.requestRef = requestAnimationFrame(() => {
this.timer = setTimeout(() => {
this.setState(
{
activeData: getActiveData(),
},
() => {
this.loopData();
}
);
}, 1000);
});
};
render() {
const { activeData = [] } = this.state;
return (
<div className={styles.activeChart}>
<NumberInfo subTitle="目标评估" total="有望达到预期" />
<div style={{ marginTop: 32 }}>
<MiniArea
animate={false}
line
borderWidth={2}
height={84}
scale={{
y: {
tickCount: 3,
},
}}
yAxis={{
tickLine: false,
label: false,
title: false,
line: false,
}}
data={activeData}
/>
</div>
{activeData && (
<div className={styles.activeChartGrid}>
<p>{[...activeData].sort()[activeData.length - 1].y + 200} 亿元</p>
<p>{[...activeData].sort()[Math.floor(activeData.length / 2)].y} 亿元</p>
</div>
)}
{activeData && (
<div className={styles.activeChartLegend}>
<span>00:00</span>
<span>{activeData[Math.floor(activeData.length / 2)].x}</span>
<span>{activeData[activeData.length - 1].x}</span>
</div>
)}
</div>
);
}
}

View File

@ -0,0 +1,31 @@
.activeChart {
position: relative;
}
.activeChartGrid {
p {
position: absolute;
top: 80px;
}
p:last-child {
top: 115px;
}
}
.activeChartLegend {
position: relative;
font-size: 0;
margin-top: 8px;
height: 20px;
line-height: 20px;
span {
display: inline-block;
font-size: 12px;
text-align: center;
width: 33.33%;
}
span:first-child {
text-align: left;
}
span:last-child {
text-align: right;
}
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import moment from 'moment';
import { Avatar } from 'antd';
import styles from './index.less';
const ArticleListContent = ({ data: { content, updatedAt, avatar, owner, href } }) => (
<div className={styles.listContent}>
<div className={styles.description}>{content}</div>
<div className={styles.extra}>
<Avatar src={avatar} size="small" />
<a href={href}>{owner}</a> <a href={href}>{href}</a>
<em>{moment(updatedAt).format('YYYY-MM-DD HH:mm')}</em>
</div>
</div>
);
export default ArticleListContent;

View File

@ -0,0 +1,38 @@
@import '~antd/lib/style/themes/default.less';
.listContent {
.description {
line-height: 22px;
max-width: 720px;
}
.extra {
color: @text-color-secondary;
margin-top: 16px;
line-height: 22px;
& > :global(.ant-avatar) {
vertical-align: top;
margin-right: 8px;
width: 20px;
height: 20px;
position: relative;
top: 1px;
}
& > em {
color: @disabled-color;
font-style: normal;
margin-left: 16px;
}
}
}
@media screen and (max-width: @screen-xs) {
.listContent {
.extra {
& > em {
display: block;
margin-left: 0;
margin-top: 8px;
}
}
}
}

View File

@ -0,0 +1,8 @@
import CheckPermissions from './CheckPermissions';
const Authorized = ({ children, authority, noMatch = null }) => {
const childrenRender = typeof children === 'undefined' ? null : children;
return CheckPermissions(authority, childrenRender, noMatch);
};
export default Authorized;

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import Authorized from './Authorized';
// TODO: umi只会返回render和rest
const AuthorizedRoute = ({ component: Component, render, authority, redirectPath, ...rest }) => (
<Authorized
authority={authority}
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
>
<Route {...rest} render={props => (Component ? <Component {...props} /> : render(props))} />
</Authorized>
);
export default AuthorizedRoute;

View File

@ -0,0 +1,88 @@
import React from 'react';
import PromiseRender from './PromiseRender';
import { CURRENT } from './renderAuthorize';
function isPromise(obj) {
return (
!!obj &&
(typeof obj === 'object' || typeof obj === 'function') &&
typeof obj.then === 'function'
);
}
/**
* 通用权限检查方法
* Common check permissions method
* @param { 权限判定 Permission judgment type string |array | Promise | Function } authority
* @param { 你的权限 Your permission description type:string} currentAuthority
* @param { 通过的组件 Passing components } target
* @param { 未通过的组件 no pass components } Exception
*/
const checkPermissions = (authority, currentAuthority, target, Exception) => {
// 没有判定权限.默认查看所有
// Retirement authority, return target;
if (!authority) {
return target;
}
// 数组处理
if (Array.isArray(authority)) {
if (authority.indexOf(currentAuthority) >= 0) {
return target;
}
if (Array.isArray(currentAuthority)) {
for (let i = 0; i < currentAuthority.length; i += 1) {
const element = currentAuthority[i];
if (authority.indexOf(element) >= 0) {
return target;
}
}
}
return Exception;
}
// string 处理
if (typeof authority === 'string') {
if (authority === currentAuthority) {
return target;
}
if (Array.isArray(currentAuthority)) {
for (let i = 0; i < currentAuthority.length; i += 1) {
const element = currentAuthority[i];
if (authority === element) {
return target;
}
}
}
return Exception;
}
// Promise 处理
if (isPromise(authority)) {
return <PromiseRender ok={target} error={Exception} promise={authority} />;
}
// Function 处理
if (typeof authority === 'function') {
try {
const bool = authority(currentAuthority);
// 函数执行后返回值是 Promise
if (isPromise(bool)) {
return <PromiseRender ok={target} error={Exception} promise={bool} />;
}
if (bool) {
return target;
}
return Exception;
} catch (error) {
throw error;
}
}
throw new Error('unsupported parameters');
};
export { checkPermissions };
const check = (authority, target, Exception) =>
checkPermissions(authority, CURRENT, target, Exception);
export default check;

View File

@ -0,0 +1,55 @@
import { checkPermissions } from './CheckPermissions';
const target = 'ok';
const error = 'error';
describe('test CheckPermissions', () => {
it('Correct string permission authentication', () => {
expect(checkPermissions('user', 'user', target, error)).toEqual('ok');
});
it('Correct string permission authentication', () => {
expect(checkPermissions('user', 'NULL', target, error)).toEqual('error');
});
it('authority is undefined , return ok', () => {
expect(checkPermissions(null, 'NULL', target, error)).toEqual('ok');
});
it('currentAuthority is undefined , return error', () => {
expect(checkPermissions('admin', null, target, error)).toEqual('error');
});
it('Wrong string permission authentication', () => {
expect(checkPermissions('admin', 'user', target, error)).toEqual('error');
});
it('Correct Array permission authentication', () => {
expect(checkPermissions(['user', 'admin'], 'user', target, error)).toEqual('ok');
});
it('Wrong Array permission authentication,currentAuthority error', () => {
expect(checkPermissions(['user', 'admin'], 'user,admin', target, error)).toEqual('error');
});
it('Wrong Array permission authentication', () => {
expect(checkPermissions(['user', 'admin'], 'guest', target, error)).toEqual('error');
});
it('Wrong Function permission authentication', () => {
expect(checkPermissions(() => false, 'guest', target, error)).toEqual('error');
});
it('Correct Function permission authentication', () => {
expect(checkPermissions(() => true, 'guest', target, error)).toEqual('ok');
});
it('authority is string, currentAuthority is array, return ok', () => {
expect(checkPermissions('user', ['user'], target, error)).toEqual('ok');
});
it('authority is string, currentAuthority is array, return ok', () => {
expect(checkPermissions('user', ['user', 'admin'], target, error)).toEqual('ok');
});
it('authority is array, currentAuthority is array, return ok', () => {
expect(checkPermissions(['user', 'admin'], ['user', 'admin'], target, error)).toEqual('ok');
});
it('Wrong Function permission authentication', () => {
expect(checkPermissions(() => false, ['user'], target, error)).toEqual('error');
});
it('Correct Function permission authentication', () => {
expect(checkPermissions(() => true, ['user'], target, error)).toEqual('ok');
});
it('authority is undefined , return ok', () => {
expect(checkPermissions(null, ['user'], target, error)).toEqual('ok');
});
});

View File

@ -0,0 +1,65 @@
import React from 'react';
import { Spin } from 'antd';
export default class PromiseRender extends React.PureComponent {
state = {
component: null,
};
componentDidMount() {
this.setRenderComponent(this.props);
}
componentDidUpdate(nextProps) {
// new Props enter
this.setRenderComponent(nextProps);
}
// set render Component : ok or error
setRenderComponent(props) {
const ok = this.checkIsInstantiation(props.ok);
const error = this.checkIsInstantiation(props.error);
props.promise
.then(() => {
this.setState({
component: ok,
});
})
.catch(() => {
this.setState({
component: error,
});
});
}
// Determine whether the incoming component has been instantiated
// AuthorizedRoute is already instantiated
// Authorized render is already instantiated, children is no instantiated
// Secured is not instantiated
checkIsInstantiation = target => {
if (!React.isValidElement(target)) {
return target;
}
return () => target;
};
render() {
const { component: Component } = this.state;
const { ok, error, promise, ...rest } = this.props;
return Component ? (
<Component {...rest} />
) : (
<div
style={{
width: '100%',
height: '100%',
margin: 'auto',
paddingTop: 50,
textAlign: 'center',
}}
>
<Spin size="large" />
</div>
);
}
}

View File

@ -0,0 +1,55 @@
import React from 'react';
import Exception from '../Exception';
import CheckPermissions from './CheckPermissions';
/**
* 默认不能访问任何页面
* default is "NULL"
*/
const Exception403 = () => <Exception type="403" />;
// Determine whether the incoming component has been instantiated
// AuthorizedRoute is already instantiated
// Authorized render is already instantiated, children is no instantiated
// Secured is not instantiated
const checkIsInstantiation = target => {
if (!React.isValidElement(target)) {
return target;
}
return () => target;
};
/**
* 用于判断是否拥有权限访问此view权限
* authority 支持传入 string, function:()=>boolean|Promise
* e.g. 'user' 只有user用户能访问
* e.g. 'user,admin' user和 admin 都能访问
* e.g. ()=>boolean 返回true能访问,返回false不能访问
* e.g. Promise then 能访问 catch不能访问
* e.g. authority support incoming string, function: () => boolean | Promise
* e.g. 'user' only user user can access
* e.g. 'user, admin' user and admin can access
* e.g. () => boolean true to be able to visit, return false can not be accessed
* e.g. Promise then can not access the visit to catch
* @param {string | function | Promise} authority
* @param {ReactNode} error 非必需参数
*/
const authorize = (authority, error) => {
/**
* conversion into a class
* 防止传入字符串时找不到staticContext造成报错
* String parameters can cause staticContext not found error
*/
let classError = false;
if (error) {
classError = () => error;
}
if (!authority) {
throw new Error('authority is required');
}
return function decideAuthority(target) {
const component = CheckPermissions(authority, target, classError || Exception403);
return checkIsInstantiation(component);
};
};
export default authorize;

View File

@ -0,0 +1,23 @@
---
order: 1
title:
zh-CN: 使用数组作为参数
en-US: Use Array as a parameter
---
Use Array as a parameter
```jsx
import RenderAuthorized from 'ant-design-pro/lib/Authorized';
import { Alert } from 'antd';
const Authorized = RenderAuthorized('user');
const noMatch = <Alert message="No permission." type="error" showIcon />;
ReactDOM.render(
<Authorized authority={['user', 'admin']} noMatch={noMatch}>
<Alert message="Use Array as a parameter passed!" type="success" showIcon />
</Authorized>,
mountNode,
);
```

View File

@ -0,0 +1,31 @@
---
order: 2
title:
zh-CN: 使用方法作为参数
en-US: Use function as a parameter
---
Use Function as a parameter
```jsx
import RenderAuthorized from 'ant-design-pro/lib/Authorized';
import { Alert } from 'antd';
const Authorized = RenderAuthorized('user');
const noMatch = <Alert message="No permission." type="error" showIcon />;
const havePermission = () => {
return false;
};
ReactDOM.render(
<Authorized authority={havePermission} noMatch={noMatch}>
<Alert
message="Use Function as a parameter passed!"
type="success"
showIcon
/>
</Authorized>,
mountNode,
);
```

View File

@ -0,0 +1,25 @@
---
order: 0
title:
zh-CN: 基本使用
en-US: Basic use
---
Basic use
```jsx
import RenderAuthorized from 'ant-design-pro/lib/Authorized';
import { Alert } from 'antd';
const Authorized = RenderAuthorized('user');
const noMatch = <Alert message="No permission." type="error" showIcon />;
ReactDOM.render(
<div>
<Authorized authority="admin" noMatch={noMatch}>
<Alert message="user Passed!" type="success" showIcon />
</Authorized>
</div>,
mountNode,
);
```

View File

@ -0,0 +1,28 @@
---
order: 3
title:
zh-CN: 注解基本使用
en-US: Basic use secured
---
secured demo used
```jsx
import RenderAuthorized from 'ant-design-pro/lib/Authorized';
import { Alert } from 'antd';
const { Secured } = RenderAuthorized('user');
@Secured('admin')
class TestSecuredString extends React.Component {
render() {
<Alert message="user Passed!" type="success" showIcon />;
}
}
ReactDOM.render(
<div>
<TestSecuredString />
</div>,
mountNode,
);
```

View File

@ -0,0 +1,43 @@
import * as React from 'react';
import { RouteProps } from 'react-router';
type authorityFN = (currentAuthority?: string) => boolean;
type authority = string | Array<string> | authorityFN | Promise<any>;
export type IReactComponent<P = any> =
| React.StatelessComponent<P>
| React.ComponentClass<P>
| React.ClassicComponentClass<P>;
interface Secured {
(authority: authority, error?: React.ReactNode): <T extends IReactComponent>(target: T) => T;
}
export interface AuthorizedRouteProps extends RouteProps {
authority: authority;
}
export class AuthorizedRoute extends React.Component<AuthorizedRouteProps, any> {}
interface check {
<T extends IReactComponent, S extends IReactComponent>(
authority: authority,
target: T,
Exception: S
): T | S;
}
export interface AuthorizedProps {
authority: authority;
noMatch?: React.ReactNode;
}
export class Authorized extends React.Component<AuthorizedProps, any> {
static Secured: Secured;
static AuthorizedRoute: typeof AuthorizedRoute;
static check: check;
}
declare function renderAuthorize(currentAuthority: string): typeof Authorized;
export default renderAuthorize;

View File

@ -0,0 +1,11 @@
import Authorized from './Authorized';
import AuthorizedRoute from './AuthorizedRoute';
import Secured from './Secured';
import check from './CheckPermissions';
import renderAuthorize from './renderAuthorize';
Authorized.Secured = Secured;
Authorized.AuthorizedRoute = AuthorizedRoute;
Authorized.check = check;
export default renderAuthorize(Authorized);

View File

@ -0,0 +1,58 @@
---
title:
en-US: Authorized
zh-CN: Authorized
subtitle: 权限
cols: 1
order: 15
---
权限组件,通过比对现有权限与准入权限,决定相关元素的展示。
## API
### RenderAuthorized
`RenderAuthorized: (currentAuthority: string | () => string) => Authorized`
权限组件默认 export RenderAuthorized 函数,它接收当前权限作为参数,返回一个权限对象,该对象提供以下几种使用方式。
### Authorized
最基础的权限控制。
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| children | 正常渲染的元素,权限判断通过时展示 | ReactNode | - |
| authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean | Promise` | - |
| noMatch | 权限异常渲染元素,权限判断不通过时展示 | ReactNode | - |
### Authorized.AuthorizedRoute
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean | Promise` | - |
| redirectPath | 权限异常时重定向的页面路由 | string | - |
其余参数与 `Route` 相同。
### Authorized.Secured
注解方式,`@Authorized.Secured(authority, error)`
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean | Promise` | - |
| error | 权限异常时渲染元素 | ReactNode | <Exception type="403" /> |
### Authorized.check
函数形式的 Authorized用于某些不能被 HOC 包裹的组件。 `Authorized.check(authority, target, Exception)`
注意:传入一个 Promise 时,无论正确还是错误返回的都是一个 ReactClass。
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean | Promise` | - |
| target | 权限判断通过时渲染的元素 | ReactNode | - |
| Exception | 权限异常时渲染元素 | ReactNode | - |

View File

@ -0,0 +1,25 @@
/* eslint-disable import/no-mutable-exports */
let CURRENT = 'NULL';
/**
* use authority or getAuthority
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = Authorized => currentAuthority => {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (
Object.prototype.toString.call(currentAuthority) === '[object String]' ||
Array.isArray(currentAuthority)
) {
CURRENT = currentAuthority;
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default Authorized => renderAuthorize(Authorized);

View File

@ -0,0 +1,10 @@
import * as React from 'react';
export interface IAvatarItemProps {
tips: React.ReactNode;
src: string;
style?: React.CSSProperties;
}
export default class AvatarItem extends React.Component<IAvatarItemProps, any> {
constructor(props: IAvatarItemProps);
}

View File

@ -0,0 +1,20 @@
---
order: 0
title:
zh-CN: 基础样例
en-US: Basic Usage
---
Simplest of usage.
````jsx
import AvatarList from 'ant-design-pro/lib/AvatarList';
ReactDOM.render(
<AvatarList size="mini">
<AvatarList.Item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<AvatarList.Item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<AvatarList.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</AvatarList>
, mountNode);
````

View File

@ -0,0 +1,12 @@
import * as React from 'react';
import AvatarItem from './AvatarItem';
export interface IAvatarListProps {
size?: 'large' | 'small' | 'mini' | 'default';
style?: React.CSSProperties;
children: React.ReactElement<AvatarItem> | Array<React.ReactElement<AvatarItem>>;
}
export default class AvatarList extends React.Component<IAvatarListProps, any> {
public static Item: typeof AvatarItem;
}

View File

@ -0,0 +1,22 @@
---
title: AvatarList
order: 1
cols: 1
---
A list of user's avatar for project or group member list frequently. If a large or small AvatarList is desired, set the `size` property to either `large` or `small` and `mini` respectively. Omit the `size` property for a AvatarList with the default size.
## API
### AvatarList
| Property | Description | Type | Default |
|----------|------------------------------------------|-------------|-------|
| size | size of list | `large`、`small` 、`mini`, `default` | `default` |
### AvatarList.Item
| Property | Description | Type | Default |
|----------|------------------------------------------|-------------|-------|
| tips | title tips for avatar item | ReactNode | - |
| src | the address of the image for an image avatar | string | - |

View File

@ -0,0 +1,43 @@
import React from 'react';
import { Tooltip, Avatar } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
const AvatarList = ({ children, size, ...other }) => {
const childrenWithProps = React.Children.map(children, child =>
React.cloneElement(child, {
size,
})
);
return (
<div {...other} className={styles.avatarList}>
<ul> {childrenWithProps} </ul>
</div>
);
};
const Item = ({ src, size, tips, onClick = () => {} }) => {
const cls = classNames(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',
});
return (
<li className={cls} onClick={onClick}>
{tips ? (
<Tooltip title={tips}>
<Avatar src={src} size={size} style={{ cursor: 'pointer' }} />
</Tooltip>
) : (
<Avatar src={src} size={size} />
)}
</li>
);
};
AvatarList.Item = Item;
export default AvatarList;

View File

@ -0,0 +1,45 @@
@import '~antd/lib/style/themes/default.less';
.avatarList {
display: inline-block;
ul {
display: inline-block;
margin-left: 8px;
font-size: 0;
}
}
.avatarItem {
display: inline-block;
font-size: @font-size-base;
margin-left: -8px;
width: @avatar-size-base;
height: @avatar-size-base;
:global {
.ant-avatar {
border: 1px solid #fff;
}
}
}
.avatarItemLarge {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
.avatarItemSmall {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
.avatarItemMini {
width: 20px;
height: 20px;
:global {
.ant-avatar {
width: 20px;
height: 20px;
line-height: 20px;
}
}
}

View File

@ -0,0 +1,23 @@
---
title: AvatarList
subtitle: 用户头像列表
order: 1
cols: 1
---
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
## API
### AvatarList
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` |
### AvatarList.Item
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| tips | 头像展示文案 | ReactNode | - |
| src | 头像图片连接 | string | - |

View File

@ -0,0 +1,15 @@
import * as React from 'react';
export interface IBarProps {
title: React.ReactNode;
color?: string;
padding?: [number, number, number, number];
height: number;
data: Array<{
x: string;
y: number;
}>;
autoLabel?: boolean;
style?: React.CSSProperties;
}
export default class Bar extends React.Component<IBarProps, any> {}

View File

@ -0,0 +1,113 @@
import React, { Component } from 'react';
import { Chart, Axis, Tooltip, Geom } from 'bizcharts';
import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind';
import autoHeight from '../autoHeight';
import styles from '../index.less';
@autoHeight()
class Bar extends Component {
state = {
autoHideXLabels: false,
};
componentDidMount() {
window.addEventListener('resize', this.resize, { passive: true });
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
handleRoot = n => {
this.root = n;
};
handleRef = n => {
this.node = n;
};
@Bind()
@Debounce(400)
resize() {
if (!this.node) {
return;
}
const canvasWidth = this.node.parentNode.clientWidth;
const { data = [], autoLabel = true } = this.props;
if (!autoLabel) {
return;
}
const minWidth = data.length * 30;
const { autoHideXLabels } = this.state;
if (canvasWidth <= minWidth) {
if (!autoHideXLabels) {
this.setState({
autoHideXLabels: true,
});
}
} else if (autoHideXLabels) {
this.setState({
autoHideXLabels: false,
});
}
}
render() {
const {
height,
title,
forceFit = true,
data,
color = 'rgba(24, 144, 255, 0.85)',
padding,
} = this.props;
const { autoHideXLabels } = this.state;
const scale = {
x: {
type: 'cat',
},
y: {
min: 0,
},
};
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y,
}),
];
return (
<div className={styles.chart} style={{ height }} ref={this.handleRoot}>
<div ref={this.handleRef}>
{title && <h4 style={{ marginBottom: 20 }}>{title}</h4>}
<Chart
scale={scale}
height={title ? height - 41 : height}
forceFit={forceFit}
data={data}
padding={padding || 'auto'}
>
<Axis
name="x"
title={false}
label={autoHideXLabels ? false : {}}
tickLine={autoHideXLabels ? false : {}}
/>
<Axis name="y" min={0} />
<Tooltip showTitle={false} crosshairs={false} />
<Geom type="interval" position="x*y" color={color} tooltip={tooltip} />
</Chart>
</div>
</div>
);
}
}
export default Bar;

View File

@ -0,0 +1,14 @@
import * as React from 'react';
import { CardProps } from 'antd/lib/card';
export interface IChartCardProps extends CardProps {
title: React.ReactNode;
action?: React.ReactNode;
total?: React.ReactNode | number | (() => React.ReactNode | number);
footer?: React.ReactNode;
contentHeight?: number;
avatar?: React.ReactNode;
style?: React.CSSProperties;
}
export default class ChartCard extends React.Component<IChartCardProps, any> {}

View File

@ -0,0 +1,82 @@
import React from 'react';
import { Card } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
const renderTotal = total => {
let totalDom;
switch (typeof total) {
case 'undefined':
totalDom = null;
break;
case 'function':
totalDom = <div className={styles.total}>{total()}</div>;
break;
default:
totalDom = <div className={styles.total}>{total}</div>;
}
return totalDom;
};
class ChartCard extends React.PureComponent {
renderConnet = () => {
const { contentHeight, title, avatar, action, total, footer, children, loading } = this.props;
if (loading) {
return false;
}
return (
<div className={styles.chartCard}>
<div
className={classNames(styles.chartTop, {
[styles.chartTopMargin]: !children && !footer,
})}
>
<div className={styles.avatar}>{avatar}</div>
<div className={styles.metaWrap}>
<div className={styles.meta}>
<span className={styles.title}>{title}</span>
<span className={styles.action}>{action}</span>
</div>
{renderTotal(total)}
</div>
</div>
{children && (
<div className={styles.content} style={{ height: contentHeight || 'auto' }}>
<div className={contentHeight && styles.contentFixed}>{children}</div>
</div>
)}
{footer && (
<div
className={classNames(styles.footer, {
[styles.footerMargin]: !children,
})}
>
{footer}
</div>
)}
</div>
);
};
render() {
const {
loading = false,
contentHeight,
title,
avatar,
action,
total,
footer,
children,
...rest
} = this.props;
return (
<Card loading={loading} bodyStyle={{ padding: '20px 24px 8px 24px' }} {...rest}>
{this.renderConnet()}
</Card>
);
}
}
export default ChartCard;

View File

@ -0,0 +1,74 @@
@import '~antd/lib/style/themes/default.less';
.chartCard {
position: relative;
.chartTop {
position: relative;
overflow: hidden;
width: 100%;
}
.chartTopMargin {
margin-bottom: 12px;
}
.chartTopHasMargin {
margin-bottom: 20px;
}
.metaWrap {
float: left;
}
.avatar {
position: relative;
top: 4px;
float: left;
margin-right: 20px;
img {
border-radius: 100%;
}
}
.meta {
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
height: 22px;
}
.action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: @heading-color;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
.content {
margin-bottom: 12px;
position: relative;
width: 100%;
}
.contentFixed {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
.footer {
border-top: 1px solid @border-color-split;
padding-top: 9px;
margin-top: 8px;
& > * {
position: relative;
}
}
.footerMargin {
margin-top: 20px;
}
}

View File

@ -0,0 +1,8 @@
import * as React from 'react';
export interface IFieldProps {
label: React.ReactNode;
value: React.ReactNode;
style?: React.CSSProperties;
}
export default class Field extends React.Component<IFieldProps, any> {}

View File

@ -0,0 +1,12 @@
import React from 'react';
import styles from './index.less';
const Field = ({ label, value, ...rest }) => (
<div className={styles.field} {...rest}>
<span>{label}</span>
<span>{value}</span>
</div>
);
export default Field;

View File

@ -0,0 +1,16 @@
@import '~antd/lib/style/themes/default.less';
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
span {
font-size: @font-size-base;
line-height: 22px;
}
span:last-child {
margin-left: 8px;
color: @heading-color;
}
}

View File

@ -0,0 +1,11 @@
import * as React from 'react';
export interface IGaugeProps {
title: React.ReactNode;
color?: string;
height: number;
bgColor?: number;
percent: number;
style?: React.CSSProperties;
}
export default class Gauge extends React.Component<IGaugeProps, any> {}

View File

@ -0,0 +1,167 @@
import React from 'react';
import { Chart, Geom, Axis, Coord, Guide, Shape } from 'bizcharts';
import autoHeight from '../autoHeight';
const { Arc, Html, Line } = Guide;
const defaultFormatter = val => {
switch (val) {
case '2':
return '差';
case '4':
return '中';
case '6':
return '良';
case '8':
return '优';
default:
return '';
}
};
Shape.registerShape('point', 'pointer', {
drawShape(cfg, group) {
let point = cfg.points[0];
point = this.parsePoint(point);
const center = this.parsePoint({
x: 0,
y: 0,
});
group.addShape('line', {
attrs: {
x1: center.x,
y1: center.y,
x2: point.x,
y2: point.y,
stroke: cfg.color,
lineWidth: 2,
lineCap: 'round',
},
});
return group.addShape('circle', {
attrs: {
x: center.x,
y: center.y,
r: 6,
stroke: cfg.color,
lineWidth: 3,
fill: '#fff',
},
});
},
});
@autoHeight()
class Gauge extends React.Component {
render() {
const {
title,
height,
percent,
forceFit = true,
formatter = defaultFormatter,
color = '#2F9CFF',
bgColor = '#F0F2F5',
} = this.props;
const cols = {
value: {
type: 'linear',
min: 0,
max: 10,
tickCount: 6,
nice: true,
},
};
const data = [{ value: percent / 10 }];
return (
<Chart height={height} data={data} scale={cols} padding={[-16, 0, 16, 0]} forceFit={forceFit}>
<Coord type="polar" startAngle={-1.25 * Math.PI} endAngle={0.25 * Math.PI} radius={0.8} />
<Axis name="1" line={null} />
<Axis
line={null}
tickLine={null}
subTickLine={null}
name="value"
zIndex={2}
gird={null}
label={{
offset: -12,
formatter,
textStyle: {
fontSize: 12,
fill: 'rgba(0, 0, 0, 0.65)',
textAlign: 'center',
},
}}
/>
<Guide>
<Line
start={[3, 0.905]}
end={[3, 0.85]}
lineStyle={{
stroke: color,
lineDash: null,
lineWidth: 2,
}}
/>
<Line
start={[5, 0.905]}
end={[5, 0.85]}
lineStyle={{
stroke: color,
lineDash: null,
lineWidth: 3,
}}
/>
<Line
start={[7, 0.905]}
end={[7, 0.85]}
lineStyle={{
stroke: color,
lineDash: null,
lineWidth: 3,
}}
/>
<Arc
zIndex={0}
start={[0, 0.965]}
end={[10, 0.965]}
style={{
stroke: bgColor,
lineWidth: 10,
}}
/>
<Arc
zIndex={1}
start={[0, 0.965]}
end={[data[0].value, 0.965]}
style={{
stroke: color,
lineWidth: 10,
}}
/>
<Html
position={['50%', '95%']}
html={() => `
<div style="width: 300px;text-align: center;font-size: 12px!important;">
<p style="font-size: 14px; color: rgba(0,0,0,0.43);margin: 0;">${title}</p>
<p style="font-size: 24px;color: rgba(0,0,0,0.85);margin: 0;">
${data[0].value * 10}%
</p>
</div>`}
/>
</Guide>
<Geom
line={false}
type="point"
position="value*1"
shape="pointer"
color={color}
active={false}
/>
</Chart>
);
}
}
export default Gauge;

View File

@ -0,0 +1,29 @@
import * as React from 'react';
// g2已经更新到3.0
// 不带的写了
export interface IAxis {
title: any;
line: any;
gridAlign: any;
labels: any;
tickLine: any;
grid: any;
}
export interface IMiniAreaProps {
color?: string;
height: number;
borderColor?: string;
line?: boolean;
animate?: boolean;
xAxis?: IAxis;
yAxis?: IAxis;
data: Array<{
x: number | string;
y: number;
}>;
}
export default class MiniArea extends React.Component<IMiniAreaProps, any> {}

View File

@ -0,0 +1,108 @@
import React from 'react';
import { Chart, Axis, Tooltip, Geom } from 'bizcharts';
import autoHeight from '../autoHeight';
import styles from '../index.less';
@autoHeight()
class MiniArea extends React.PureComponent {
render() {
const {
height,
data = [],
forceFit = true,
color = 'rgba(24, 144, 255, 0.2)',
borderColor = '#1089ff',
scale = {},
borderWidth = 2,
line,
xAxis,
yAxis,
animate = true,
} = this.props;
const padding = [36, 5, 30, 5];
const scaleProps = {
x: {
type: 'cat',
range: [0, 1],
...scale.x,
},
y: {
min: 0,
...scale.y,
},
};
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y,
}),
];
const chartHeight = height + 54;
return (
<div className={styles.miniChart} style={{ height }}>
<div className={styles.chartContent}>
{height > 0 && (
<Chart
animate={animate}
scale={scaleProps}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
>
<Axis
key="axis-x"
name="x"
label={false}
line={false}
tickLine={false}
grid={false}
{...xAxis}
/>
<Axis
key="axis-y"
name="y"
label={false}
line={false}
tickLine={false}
grid={false}
{...yAxis}
/>
<Tooltip showTitle={false} crosshairs={false} />
<Geom
type="area"
position="x*y"
color={color}
tooltip={tooltip}
shape="smooth"
style={{
fillOpacity: 1,
}}
/>
{line ? (
<Geom
type="line"
position="x*y"
shape="smooth"
color={borderColor}
size={borderWidth}
tooltip={false}
/>
) : (
<span style={{ display: 'none' }} />
)}
</Chart>
)}
</div>
</div>
);
}
}
export default MiniArea;

View File

@ -0,0 +1,12 @@
import * as React from 'react';
export interface IMiniBarProps {
color?: string;
height: number;
data: Array<{
x: number | string;
y: number;
}>;
style?: React.CSSProperties;
}
export default class MiniBar extends React.Component<IMiniBarProps, any> {}

View File

@ -0,0 +1,51 @@
import React from 'react';
import { Chart, Tooltip, Geom } from 'bizcharts';
import autoHeight from '../autoHeight';
import styles from '../index.less';
@autoHeight()
class MiniBar extends React.Component {
render() {
const { height, forceFit = true, color = '#1890FF', data = [] } = this.props;
const scale = {
x: {
type: 'cat',
},
y: {
min: 0,
},
};
const padding = [36, 5, 30, 5];
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y,
}),
];
// for tooltip not to be hide
const chartHeight = height + 54;
return (
<div className={styles.miniChart} style={{ height }}>
<div className={styles.chartContent}>
<Chart
scale={scale}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
>
<Tooltip showTitle={false} crosshairs={false} />
<Geom type="interval" position="x*y" color={color} tooltip={tooltip} />
</Chart>
</div>
</div>
);
}
}
export default MiniBar;

View File

@ -0,0 +1,10 @@
import * as React from 'react';
export interface IMiniProgressProps {
target: number;
color?: string;
strokeWidth?: number;
percent?: number;
style?: React.CSSProperties;
}
export default class MiniProgress extends React.Component<IMiniProgressProps, any> {}

View File

@ -0,0 +1,27 @@
import React from 'react';
import { Tooltip } from 'antd';
import styles from './index.less';
const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => (
<div className={styles.miniProgress}>
<Tooltip title={`目标值: ${target}%`}>
<div className={styles.target} style={{ left: target ? `${target}%` : null }}>
<span style={{ backgroundColor: color || null }} />
<span style={{ backgroundColor: color || null }} />
</div>
</Tooltip>
<div className={styles.progressWrap}>
<div
className={styles.progress}
style={{
backgroundColor: color || null,
width: percent ? `${percent}%` : null,
height: strokeWidth || null,
}}
/>
</div>
</div>
);
export default MiniProgress;

View File

@ -0,0 +1,35 @@
@import '~antd/lib/style/themes/default.less';
.miniProgress {
padding: 5px 0;
position: relative;
width: 100%;
.progressWrap {
background-color: @background-color-base;
position: relative;
}
.progress {
transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
border-radius: 1px 0 0 1px;
background-color: @primary-color;
width: 0;
height: 100%;
}
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
}
span:last-child {
top: auto;
bottom: 0;
}
}
}

View File

@ -0,0 +1,21 @@
import * as React from 'react';
export interface IPieProps {
animate?: boolean;
color?: string;
colors?: string[];
height: number;
hasLegend?: boolean;
padding?: [number, number, number, number];
percent?: number;
data?: Array<{
x: string | string;
y: number;
}>;
total?: React.ReactNode | number | (() => React.ReactNode | number);
title?: React.ReactNode;
tooltip?: boolean;
valueFormat?: (value: string) => string | React.ReactNode;
subTitle?: React.ReactNode;
}
export default class Pie extends React.Component<IPieProps, any> {}

View File

@ -0,0 +1,271 @@
import React, { Component } from 'react';
import { Chart, Tooltip, Geom, Coord } from 'bizcharts';
import { DataView } from '@antv/data-set';
import { Divider } from 'antd';
import classNames from 'classnames';
import ReactFitText from 'react-fittext';
import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind';
import autoHeight from '../autoHeight';
import styles from './index.less';
/* eslint react/no-danger:0 */
@autoHeight()
class Pie extends Component {
state = {
legendData: [],
legendBlock: false,
};
componentDidMount() {
window.addEventListener(
'resize',
() => {
this.requestRef = requestAnimationFrame(() => this.resize());
},
{ passive: true }
);
}
componentDidUpdate(preProps) {
const { data } = this.props;
if (data !== preProps.data) {
// because of charts data create when rendered
// so there is a trick for get rendered time
this.getLegendData();
}
}
componentWillUnmount() {
window.cancelAnimationFrame(this.requestRef);
window.removeEventListener('resize', this.resize);
this.resize.cancel();
}
getG2Instance = chart => {
this.chart = chart;
requestAnimationFrame(() => {
this.getLegendData();
this.resize();
});
};
// for custom lengend view
getLegendData = () => {
if (!this.chart) return;
const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
if (!geom) return;
const items = geom.get('dataArray') || []; // 获取图形对应的
const legendData = items.map(item => {
/* eslint no-underscore-dangle:0 */
const origin = item[0]._origin;
origin.color = item[0].color;
origin.checked = true;
return origin;
});
this.setState({
legendData,
});
};
handleRoot = n => {
this.root = n;
};
handleLegendClick = (item, i) => {
const newItem = item;
newItem.checked = !newItem.checked;
const { legendData } = this.state;
legendData[i] = newItem;
const filteredLegendData = legendData.filter(l => l.checked).map(l => l.x);
if (this.chart) {
this.chart.filter('x', val => filteredLegendData.indexOf(val) > -1);
}
this.setState({
legendData,
});
};
// for window resize auto responsive legend
@Bind()
@Debounce(300)
resize() {
const { hasLegend } = this.props;
const { legendBlock } = this.state;
if (!hasLegend || !this.root) {
window.removeEventListener('resize', this.resize);
return;
}
if (this.root.parentNode.clientWidth <= 380) {
if (!legendBlock) {
this.setState({
legendBlock: true,
});
}
} else if (legendBlock) {
this.setState({
legendBlock: false,
});
}
}
render() {
const {
valueFormat,
subTitle,
total,
hasLegend = false,
className,
style,
height,
forceFit = true,
percent,
color,
inner = 0.75,
animate = true,
colors,
lineWidth = 1,
} = this.props;
const { legendData, legendBlock } = this.state;
const pieClassName = classNames(styles.pie, className, {
[styles.hasLegend]: !!hasLegend,
[styles.legendBlock]: legendBlock,
});
const {
data: propsData,
selected: propsSelected = true,
tooltip: propsTooltip = true,
} = this.props;
let data = propsData || [];
let selected = propsSelected;
let tooltip = propsTooltip;
const defaultColors = colors;
data = data || [];
selected = selected || true;
tooltip = tooltip || true;
let formatColor;
const scale = {
x: {
type: 'cat',
range: [0, 1],
},
y: {
min: 0,
},
};
if (percent || percent === 0) {
selected = false;
tooltip = false;
formatColor = value => {
if (value === '占比') {
return color || 'rgba(24, 144, 255, 0.85)';
}
return '#F0F2F5';
};
data = [
{
x: '占比',
y: parseFloat(percent),
},
{
x: '反比',
y: 100 - parseFloat(percent),
},
];
}
const tooltipFormat = [
'x*percent',
(x, p) => ({
name: x,
value: `${(p * 100).toFixed(2)}%`,
}),
];
const padding = [12, 0, 12, 0];
const dv = new DataView();
dv.source(data).transform({
type: 'percent',
field: 'y',
dimension: 'x',
as: 'percent',
});
return (
<div ref={this.handleRoot} className={pieClassName} style={style}>
<ReactFitText maxFontSize={25}>
<div className={styles.chart}>
<Chart
scale={scale}
height={height}
forceFit={forceFit}
data={dv}
padding={padding}
animate={animate}
onGetG2Instance={this.getG2Instance}
>
{!!tooltip && <Tooltip showTitle={false} />}
<Coord type="theta" innerRadius={inner} />
<Geom
style={{ lineWidth, stroke: '#fff' }}
tooltip={tooltip && tooltipFormat}
type="intervalStack"
position="percent"
color={['x', percent || percent === 0 ? formatColor : defaultColors]}
selected={selected}
/>
</Chart>
{(subTitle || total) && (
<div className={styles.total}>
{subTitle && <h4 className="pie-sub-title">{subTitle}</h4>}
{/* eslint-disable-next-line */}
{total && (
<div className="pie-stat">{typeof total === 'function' ? total() : total}</div>
)}
</div>
)}
</div>
</ReactFitText>
{hasLegend && (
<ul className={styles.legend}>
{legendData.map((item, i) => (
<li key={item.x} onClick={() => this.handleLegendClick(item, i)}>
<span
className={styles.dot}
style={{
backgroundColor: !item.checked ? '#aaa' : item.color,
}}
/>
<span className={styles.legendTitle}>{item.x}</span>
<Divider type="vertical" />
<span className={styles.percent}>
{`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}
</span>
<span className={styles.value}>{valueFormat ? valueFormat(item.y) : item.y}</span>
</li>
))}
</ul>
)}
</div>
);
}
}
export default Pie;

View File

@ -0,0 +1,94 @@
@import '~antd/lib/style/themes/default.less';
.pie {
position: relative;
.chart {
position: relative;
}
&.hasLegend .chart {
width: ~'calc(100% - 240px)';
}
.legend {
position: absolute;
right: 0;
min-width: 200px;
top: 50%;
transform: translateY(-50%);
margin: 0 20px;
list-style: none;
padding: 0;
li {
cursor: pointer;
margin-bottom: 16px;
height: 22px;
line-height: 22px;
&:last-child {
margin-bottom: 0;
}
}
}
.dot {
border-radius: 8px;
display: inline-block;
margin-right: 8px;
position: relative;
top: -1px;
height: 8px;
width: 8px;
}
.line {
background-color: @border-color-split;
display: inline-block;
margin-right: 8px;
width: 1px;
height: 16px;
}
.legendTitle {
color: @text-color;
}
.percent {
color: @text-color-secondary;
}
.value {
position: absolute;
right: 0;
}
.title {
margin-bottom: 8px;
}
.total {
position: absolute;
left: 50%;
top: 50%;
text-align: center;
max-height: 62px;
transform: translate(-50%, -50%);
& > h4 {
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
height: 22px;
margin-bottom: 8px;
font-weight: normal;
}
& > p {
color: @heading-color;
display: block;
font-size: 1.2em;
height: 32px;
line-height: 32px;
white-space: nowrap;
}
}
}
.legendBlock {
&.hasLegend .chart {
width: 100%;
margin: 0 0 32px 0;
}
.legend {
position: relative;
transform: none;
}
}

View File

@ -0,0 +1,15 @@
import * as React from 'react';
export interface IRadarProps {
title?: React.ReactNode;
height: number;
padding?: [number, number, number, number];
hasLegend?: boolean;
data: Array<{
name: string;
label: string;
value: string;
}>;
style?: React.CSSProperties;
}
export default class Radar extends React.Component<IRadarProps, any> {}

View File

@ -0,0 +1,184 @@
import React, { Component } from 'react';
import { Chart, Tooltip, Geom, Coord, Axis } from 'bizcharts';
import { Row, Col } from 'antd';
import autoHeight from '../autoHeight';
import styles from './index.less';
/* eslint react/no-danger:0 */
@autoHeight()
class Radar extends Component {
state = {
legendData: [],
};
componentDidMount() {
this.getLegendData();
}
componentDidUpdate(preProps) {
const { data } = this.props;
if (data !== preProps.data) {
this.getLegendData();
}
}
getG2Instance = chart => {
this.chart = chart;
};
// for custom lengend view
getLegendData = () => {
if (!this.chart) return;
const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
if (!geom) return;
const items = geom.get('dataArray') || []; // 获取图形对应的
const legendData = items.map(item => {
// eslint-disable-next-line
const origins = item.map(t => t._origin);
const result = {
name: origins[0].name,
color: item[0].color,
checked: true,
value: origins.reduce((p, n) => p + n.value, 0),
};
return result;
});
this.setState({
legendData,
});
};
handleRef = n => {
this.node = n;
};
handleLegendClick = (item, i) => {
const newItem = item;
newItem.checked = !newItem.checked;
const { legendData } = this.state;
legendData[i] = newItem;
const filteredLegendData = legendData.filter(l => l.checked).map(l => l.name);
if (this.chart) {
this.chart.filter('name', val => filteredLegendData.indexOf(val) > -1);
this.chart.repaint();
}
this.setState({
legendData,
});
};
render() {
const defaultColors = [
'#1890FF',
'#FACC14',
'#2FC25B',
'#8543E0',
'#F04864',
'#13C2C2',
'#fa8c16',
'#a0d911',
];
const {
data = [],
height = 0,
title,
hasLegend = false,
forceFit = true,
tickCount = 4,
padding = [35, 30, 16, 30],
animate = true,
colors = defaultColors,
} = this.props;
const { legendData } = this.state;
const scale = {
value: {
min: 0,
tickCount,
},
};
const chartHeight = height - (hasLegend ? 80 : 22);
return (
<div className={styles.radar} style={{ height }}>
{title && <h4>{title}</h4>}
<Chart
scale={scale}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
animate={animate}
onGetG2Instance={this.getG2Instance}
>
<Tooltip />
<Coord type="polar" />
<Axis
name="label"
line={null}
tickLine={null}
grid={{
lineStyle: {
lineDash: null,
},
hideFirstLine: false,
}}
/>
<Axis
name="value"
grid={{
type: 'polygon',
lineStyle: {
lineDash: null,
},
}}
/>
<Geom type="line" position="label*value" color={['name', colors]} size={1} />
<Geom
type="point"
position="label*value"
color={['name', colors]}
shape="circle"
size={3}
/>
</Chart>
{hasLegend && (
<Row className={styles.legend}>
{legendData.map((item, i) => (
<Col
span={24 / legendData.length}
key={item.name}
onClick={() => this.handleLegendClick(item, i)}
>
<div className={styles.legendItem}>
<p>
<span
className={styles.dot}
style={{
backgroundColor: !item.checked ? '#aaa' : item.color,
}}
/>
<span>{item.name}</span>
</p>
<h6>{item.value}</h6>
</div>
</Col>
))}
</Row>
)}
</div>
);
}
}
export default Radar;

View File

@ -0,0 +1,46 @@
@import '~antd/lib/style/themes/default.less';
.radar {
.legend {
margin-top: 16px;
.legendItem {
position: relative;
text-align: center;
cursor: pointer;
color: @text-color-secondary;
line-height: 22px;
p {
margin: 0;
}
h6 {
color: @heading-color;
padding-left: 16px;
font-size: 24px;
line-height: 32px;
margin-top: 4px;
margin-bottom: 0;
}
&:after {
background-color: @border-color-split;
position: absolute;
top: 8px;
right: 0;
height: 40px;
width: 1px;
content: '';
}
}
> :last-child .legendItem:after {
display: none;
}
.dot {
border-radius: 6px;
display: inline-block;
margin-right: 6px;
position: relative;
top: -1px;
height: 6px;
width: 6px;
}
}
}

View File

@ -0,0 +1,11 @@
import * as React from 'react';
export interface ITagCloudProps {
data: Array<{
name: string;
value: number;
}>;
height: number;
style?: React.CSSProperties;
}
export default class TagCloud extends React.Component<ITagCloudProps, any> {}

View File

@ -0,0 +1,170 @@
import React, { Component } from 'react';
import { Chart, Geom, Coord, Shape } from 'bizcharts';
import DataSet from '@antv/data-set';
import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind';
import classNames from 'classnames';
import autoHeight from '../autoHeight';
import styles from './index.less';
/* eslint no-underscore-dangle: 0 */
/* eslint no-param-reassign: 0 */
const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png';
@autoHeight()
class TagCloud extends Component {
state = {
dv: null,
};
componentDidMount() {
requestAnimationFrame(() => {
this.initTagCloud();
this.renderChart();
});
window.addEventListener('resize', this.resize, { passive: true });
}
componentDidUpdate(preProps) {
const { data } = this.props;
if (JSON.stringify(preProps.data) !== JSON.stringify(data)) {
this.renderChart(this.props);
}
}
componentWillUnmount() {
this.isUnmount = true;
window.cancelAnimationFrame(this.requestRef);
window.removeEventListener('resize', this.resize);
}
resize = () => {
this.requestRef = requestAnimationFrame(() => {
this.renderChart();
});
};
saveRootRef = node => {
this.root = node;
};
initTagCloud = () => {
function getTextAttrs(cfg) {
return Object.assign(
{},
{
fillOpacity: cfg.opacity,
fontSize: cfg.origin._origin.size,
rotate: cfg.origin._origin.rotate,
text: cfg.origin._origin.text,
textAlign: 'center',
fontFamily: cfg.origin._origin.font,
fill: cfg.color,
textBaseline: 'Alphabetic',
},
cfg.style
);
}
// 给point注册一个词云的shape
Shape.registerShape('point', 'cloud', {
drawShape(cfg, container) {
const attrs = getTextAttrs(cfg);
return container.addShape('text', {
attrs: Object.assign(attrs, {
x: cfg.x,
y: cfg.y,
}),
});
},
});
};
@Bind()
@Debounce(500)
renderChart(nextProps) {
// const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
const { data, height } = nextProps || this.props;
if (data.length < 1 || !this.root) {
return;
}
const h = height * 4;
const w = this.root.offsetWidth * 4;
const onload = () => {
const dv = new DataSet.View().source(data);
const range = dv.range('value');
const [min, max] = range;
dv.transform({
type: 'tag-cloud',
fields: ['name', 'value'],
imageMask: this.imageMask,
font: 'Verdana',
size: [w, h], // 宽高设置最好根据 imageMask 做调整
padding: 5,
timeInterval: 5000, // max execute time
rotate() {
return 0;
},
fontSize(d) {
// eslint-disable-next-line
return Math.pow((d.value - min) / (max - min), 2) * (70 - 20) + 20;
},
});
if (this.isUnmount) {
return;
}
this.setState({
dv,
w,
h,
});
};
if (!this.imageMask) {
this.imageMask = new Image();
this.imageMask.crossOrigin = '';
this.imageMask.src = imgUrl;
this.imageMask.onload = onload;
} else {
onload();
}
}
render() {
const { className, height } = this.props;
const { dv, w, h } = this.state;
return (
<div
className={classNames(styles.tagCloud, className)}
style={{ width: '100%', height }}
ref={this.saveRootRef}
>
{dv && (
<Chart
width={w}
height={h}
data={dv}
padding={0}
scale={{
x: { nice: false },
y: { nice: false },
}}
>
<Coord reflect="y" />
<Geom type="point" position="x*y" color="text" shape="cloud" />
</Chart>
)}
</div>
);
}
}
export default TagCloud;

View File

@ -0,0 +1,7 @@
.tagCloud {
overflow: hidden;
canvas {
transform: scale(0.25);
transform-origin: 0 0;
}
}

View File

@ -0,0 +1,14 @@
import * as React from 'react';
export interface ITimelineChartProps {
data: Array<{
x: number;
y1: number;
y2?: number;
}>;
titleMap: { y1: string; y2?: string };
padding?: [number, number, number, number];
height?: number;
style?: React.CSSProperties;
}
export default class TimelineChart extends React.Component<ITimelineChartProps, any> {}

View File

@ -0,0 +1,124 @@
import React from 'react';
import { Chart, Tooltip, Geom, Legend, Axis } from 'bizcharts';
import DataSet from '@antv/data-set';
import Slider from 'bizcharts-plugin-slider';
import autoHeight from '../autoHeight';
import styles from './index.less';
@autoHeight()
class TimelineChart extends React.Component {
render() {
const {
title,
height = 400,
padding = [60, 20, 40, 40],
titleMap = {
y1: 'y1',
y2: 'y2',
},
borderWidth = 2,
data = [
{
x: 0,
y1: 0,
y2: 0,
},
],
} = this.props;
data.sort((a, b) => a.x - b.x);
let max;
if (data[0] && data[0].y1 && data[0].y2) {
max = Math.max(
[...data].sort((a, b) => b.y1 - a.y1)[0].y1,
[...data].sort((a, b) => b.y2 - a.y2)[0].y2
);
}
const ds = new DataSet({
state: {
start: data[0].x,
end: data[data.length - 1].x,
},
});
const dv = ds.createView();
dv.source(data)
.transform({
type: 'filter',
callback: obj => {
const date = obj.x;
return date <= ds.state.end && date >= ds.state.start;
},
})
.transform({
type: 'map',
callback(row) {
const newRow = { ...row };
newRow[titleMap.y1] = row.y1;
newRow[titleMap.y2] = row.y2;
return newRow;
},
})
.transform({
type: 'fold',
fields: [titleMap.y1, titleMap.y2], // 展开字段集
key: 'key', // key字段
value: 'value', // value字段
});
const timeScale = {
type: 'time',
tickInterval: 60 * 60 * 1000,
mask: 'HH:mm',
range: [0, 1],
};
const cols = {
x: timeScale,
value: {
max,
min: 0,
},
};
const SliderGen = () => (
<Slider
padding={[0, padding[1] + 20, 0, padding[3]]}
width="auto"
height={26}
xAxis="x"
yAxis="y1"
scales={{ x: timeScale }}
data={data}
start={ds.state.start}
end={ds.state.end}
backgroundChart={{ type: 'line' }}
onChange={({ startValue, endValue }) => {
ds.setState('start', startValue);
ds.setState('end', endValue);
}}
/>
);
return (
<div className={styles.timelineChart} style={{ height: height + 30 }}>
<div>
{title && <h4>{title}</h4>}
<Chart height={height} padding={padding} data={dv} scale={cols} forceFit>
<Axis name="x" />
<Tooltip />
<Legend name="key" position="top" />
<Geom type="line" position="x*value" size={borderWidth} color="key" />
</Chart>
<div style={{ marginRight: -20 }}>
<SliderGen />
</div>
</div>
</div>
);
}
}
export default TimelineChart;

View File

@ -0,0 +1,3 @@
.timelineChart {
background: #fff;
}

View File

@ -0,0 +1,10 @@
import * as React from 'react';
export interface IWaterWaveProps {
title: React.ReactNode;
color?: string;
height: number;
percent: number;
style?: React.CSSProperties;
}
export default class WaterWave extends React.Component<IWaterWaveProps, any> {}

View File

@ -0,0 +1,213 @@
import React, { PureComponent } from 'react';
import autoHeight from '../autoHeight';
import styles from './index.less';
/* eslint no-return-assign: 0 */
/* eslint no-mixed-operators: 0 */
// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
@autoHeight()
class WaterWave extends PureComponent {
state = {
radio: 1,
};
componentDidMount() {
this.renderChart();
this.resize();
window.addEventListener(
'resize',
() => {
requestAnimationFrame(() => this.resize());
},
{ passive: true }
);
}
componentDidUpdate(props) {
const { percent } = this.props;
if (props.percent !== percent) {
// 不加这个会造成绘制缓慢
this.renderChart('update');
}
}
componentWillUnmount() {
cancelAnimationFrame(this.timer);
if (this.node) {
this.node.innerHTML = '';
}
window.removeEventListener('resize', this.resize);
}
resize = () => {
if (this.root) {
const { height } = this.props;
const { offsetWidth } = this.root.parentNode;
this.setState({
radio: offsetWidth < height ? offsetWidth / height : 1,
});
}
};
renderChart(type) {
const { percent, color = '#1890FF' } = this.props;
const data = percent / 100;
const self = this;
cancelAnimationFrame(this.timer);
if (!this.node || (data !== 0 && !data)) {
return;
}
const canvas = this.node;
const ctx = canvas.getContext('2d');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const radius = canvasWidth / 2;
const lineWidth = 2;
const cR = radius - lineWidth;
ctx.beginPath();
ctx.lineWidth = lineWidth * 2;
const axisLength = canvasWidth - lineWidth;
const unit = axisLength / 8;
const range = 0.2; // 振幅
let currRange = range;
const xOffset = lineWidth;
let sp = 0; // 周期偏移量
let currData = 0;
const waveupsp = 0.005; // 水波上涨速度
let arcStack = [];
const bR = radius - lineWidth;
const circleOffset = -(Math.PI / 2);
let circleLock = true;
for (let i = circleOffset; i < circleOffset + 2 * Math.PI; i += 1 / (8 * Math.PI)) {
arcStack.push([radius + bR * Math.cos(i), radius + bR * Math.sin(i)]);
}
const cStartPoint = arcStack.shift();
ctx.strokeStyle = color;
ctx.moveTo(cStartPoint[0], cStartPoint[1]);
function drawSin() {
ctx.beginPath();
ctx.save();
const sinStack = [];
for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
const x = sp + (xOffset + i) / unit;
const y = Math.sin(x) * currRange;
const dx = i;
const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y;
ctx.lineTo(dx, dy);
sinStack.push([dx, dy]);
}
const startPoint = sinStack.shift();
ctx.lineTo(xOffset + axisLength, canvasHeight);
ctx.lineTo(xOffset, canvasHeight);
ctx.lineTo(startPoint[0], startPoint[1]);
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(1, color);
ctx.fillStyle = gradient;
ctx.fill();
ctx.restore();
}
function render() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (circleLock && type !== 'update') {
if (arcStack.length) {
const temp = arcStack.shift();
ctx.lineTo(temp[0], temp[1]);
ctx.stroke();
} else {
circleLock = false;
ctx.lineTo(cStartPoint[0], cStartPoint[1]);
ctx.stroke();
arcStack = null;
ctx.globalCompositeOperation = 'destination-over';
ctx.beginPath();
ctx.lineWidth = lineWidth;
ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1);
ctx.beginPath();
ctx.save();
ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, 1);
ctx.restore();
ctx.clip();
ctx.fillStyle = color;
}
} else {
if (data >= 0.85) {
if (currRange > range / 4) {
const t = range * 0.01;
currRange -= t;
}
} else if (data <= 0.1) {
if (currRange < range * 1.5) {
const t = range * 0.01;
currRange += t;
}
} else {
if (currRange <= range) {
const t = range * 0.01;
currRange += t;
}
if (currRange >= range) {
const t = range * 0.01;
currRange -= t;
}
}
if (data - currData > 0) {
currData += waveupsp;
}
if (data - currData < 0) {
currData -= waveupsp;
}
sp += 0.07;
drawSin();
}
self.timer = requestAnimationFrame(render);
}
render();
}
render() {
const { radio } = this.state;
const { percent, title, height } = this.props;
return (
<div
className={styles.waterWave}
ref={n => (this.root = n)}
style={{ transform: `scale(${radio})` }}
>
<div style={{ width: height, height, overflow: 'hidden' }}>
<canvas
className={styles.waterWaveCanvasWrapper}
ref={n => (this.node = n)}
width={height * 2}
height={height * 2}
/>
</div>
<div className={styles.text} style={{ width: height }}>
{title && <span>{title}</span>}
<h4>{percent}%</h4>
</div>
</div>
);
}
}
export default WaterWave;

View File

@ -0,0 +1,28 @@
@import '~antd/lib/style/themes/default.less';
.waterWave {
display: inline-block;
position: relative;
transform-origin: left;
.text {
position: absolute;
left: 0;
top: 32px;
text-align: center;
width: 100%;
span {
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
h4 {
color: @heading-color;
line-height: 32px;
font-size: 24px;
}
}
.waterWaveCanvasWrapper {
transform: scale(0.5);
transform-origin: 0 0;
}
}

View File

@ -0,0 +1,62 @@
/* eslint eqeqeq: 0 */
import React from 'react';
function computeHeight(node) {
const totalHeight = parseInt(getComputedStyle(node).height, 10);
const padding =
parseInt(getComputedStyle(node).paddingTop, 10) +
parseInt(getComputedStyle(node).paddingBottom, 10);
return totalHeight - padding;
}
function getAutoHeight(n) {
if (!n) {
return 0;
}
let node = n;
let height = computeHeight(node);
while (!height) {
node = node.parentNode;
if (node) {
height = computeHeight(node);
} else {
break;
}
}
return height;
}
const autoHeight = () => WrappedComponent =>
class extends React.Component {
state = {
computedHeight: 0,
};
componentDidMount() {
const { height } = this.props;
if (!height) {
const h = getAutoHeight(this.root);
// eslint-disable-next-line
this.setState({ computedHeight: h });
}
}
handleRoot = node => {
this.root = node;
};
render() {
const { height } = this.props;
const { computedHeight } = this.state;
const h = height || computedHeight;
return (
<div ref={this.handleRoot}>{h > 0 && <WrappedComponent {...this.props} height={h} />}</div>
);
}
};
export default autoHeight;

View File

@ -0,0 +1,3 @@
import * as BizChart from 'bizcharts';
export = BizChart;

View File

@ -0,0 +1,3 @@
import * as BizChart from 'bizcharts';
export default BizChart;

View File

@ -0,0 +1,26 @@
---
order: 4
title: 柱状图
---
通过设置 `x``y` 属性,可以快速的构建出一个漂亮的柱状图,各种纬度的关系则是通过自定义的数据展现。
````jsx
import { Bar } from 'ant-design-pro/lib/Charts';
const salesData = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}月`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
ReactDOM.render(
<Bar
height={200}
title="销售额趋势"
data={salesData}
/>
, mountNode);
````

View File

@ -0,0 +1,95 @@
---
order: 1
title: 图表卡片
---
用于展示图表的卡片容器,可以方便的配合其它图表套件展示丰富信息。
```jsx
import { ChartCard, yuan, Field } from 'ant-design-pro/lib/Charts';
import Trend from 'ant-design-pro/lib/Trend';
import { Row, Col, Icon, Tooltip } from 'antd';
import numeral from 'numeral';
ReactDOM.render(
<Row>
<Col span={24}>
<ChartCard
title="销售额"
action={
<Tooltip title="指标说明">
<Icon type="info-circle-o" />
</Tooltip>
}
total={() => (
<span dangerouslySetInnerHTML={{ __html: yuan(126560) }} />
)}
footer={
<Field label="日均销售额" value={numeral(12423).format("0,0")} />
}
contentHeight={46}
>
<span>
周同比
<Trend flag="up" style={{ marginLeft: 8, color: "rgba(0,0,0,.85)" }}>
12%
</Trend>
</span>
<span style={{ marginLeft: 16 }}>
日环比
<Trend
flag="down"
style={{ marginLeft: 8, color: "rgba(0,0,0,.85)" }}
>
11%
</Trend>
</span>
</ChartCard>
</Col>
<Col span={24} style={{ marginTop: 24 }}>
<ChartCard
title="移动指标"
avatar={
<img
style={{ width: 56, height: 56 }}
src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png"
alt="indicator"
/>
}
action={
<Tooltip title="指标说明">
<Icon type="info-circle-o" />
</Tooltip>
}
total={() => (
<span dangerouslySetInnerHTML={{ __html: yuan(126560) }} />
)}
footer={
<Field label="日均销售额" value={numeral(12423).format("0,0")} />
}
/>
</Col>
<Col span={24} style={{ marginTop: 24 }}>
<ChartCard
title="移动指标"
avatar={
<img
alt="indicator"
style={{ width: 56, height: 56 }}
src="https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png"
/>
}
action={
<Tooltip title="指标说明">
<Icon type="info-circle-o" />
</Tooltip>
}
total={() => (
<span dangerouslySetInnerHTML={{ __html: yuan(126560) }} />
)}
/>
</Col>
</Row>,
mountNode,
);
```

View File

@ -0,0 +1,18 @@
---
order: 7
title: 仪表盘
---
仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。
````jsx
import { Gauge } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
<Gauge
title="核销率"
height={164}
percent={87}
/>
, mountNode);
````

View File

@ -0,0 +1,28 @@
---
order: 2
col: 2
title: 迷你区域图
---
````jsx
import { MiniArea } from 'ant-design-pro/lib/Charts';
import moment from 'moment';
const visitData = [];
const beginDay = new Date().getTime();
for (let i = 0; i < 20; i += 1) {
visitData.push({
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
y: Math.floor(Math.random() * 100) + 10,
});
}
ReactDOM.render(
<MiniArea
line
color="#cceafe"
height={45}
data={visitData}
/>
, mountNode);
````

View File

@ -0,0 +1,28 @@
---
order: 2
col: 2
title: 迷你柱状图
---
迷你柱状图更适合展示简单的区间数据,简洁的表现方式可以很好的减少大数据量的视觉展现压力。
````jsx
import { MiniBar } from 'ant-design-pro/lib/Charts';
import moment from 'moment';
const visitData = [];
const beginDay = new Date().getTime();
for (let i = 0; i < 20; i += 1) {
visitData.push({
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
y: Math.floor(Math.random() * 100) + 10,
});
}
ReactDOM.render(
<MiniBar
height={45}
data={visitData}
/>
, mountNode);
````

View File

@ -0,0 +1,16 @@
---
order: 6
title: 迷你饼状图
---
通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展
现更多业务场景。
```jsx
import { Pie } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
<Pie percent={28} subTitle="中式快餐" total="28%" height={140} />,
mountNode
);
```

View File

@ -0,0 +1,12 @@
---
order: 3
title: 迷你进度条
---
````jsx
import { MiniProgress } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
<MiniProgress percent={78} strokeWidth={8} target={80} />
, mountNode);
````

View File

@ -0,0 +1,84 @@
---
order: 0
title: 图表套件组合展示
---
利用 Ant Design Pro 提供的图表套件,可以灵活组合符合设计规范的图表来满足复杂的业务需求。
````jsx
import { ChartCard, Field, MiniArea, MiniBar, MiniProgress } from 'ant-design-pro/lib/Charts';
import Trend from 'ant-design-pro/lib/Trend';
import NumberInfo from 'ant-design-pro/lib/NumberInfo';
import { Row, Col, Icon, Tooltip } from 'antd';
import numeral from 'numeral';
import moment from 'moment';
const visitData = [];
const beginDay = new Date().getTime();
for (let i = 0; i < 20; i += 1) {
visitData.push({
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
y: Math.floor(Math.random() * 100) + 10,
});
}
ReactDOM.render(
<Row>
<Col span={24}>
<ChartCard
title="搜索用户数量"
total={numeral(8846).format('0,0')}
contentHeight={134}
>
<NumberInfo
subTitle={<span>本周访问</span>}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
<MiniArea
line
height={45}
data={visitData}
/>
</ChartCard>
</Col>
<Col span={24} style={{ marginTop: 24 }}>
<ChartCard
title="访问量"
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
total={numeral(8846).format('0,0')}
footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />}
contentHeight={46}
>
<MiniBar
height={46}
data={visitData}
/>
</ChartCard>
</Col>
<Col span={24} style={{ marginTop: 24 }}>
<ChartCard
title="线上购物转化率"
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
total="78%"
footer={
<div>
<span>
周同比
<Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
</span>
<span style={{ marginLeft: 16 }}>
日环比
<Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
</span>
</div>
}
contentHeight={46}
>
<MiniProgress percent={78} strokeWidth={8} target={80} />
</ChartCard>
</Col>
</Row>
, mountNode);
````

View File

@ -0,0 +1,54 @@
---
order: 5
title: 饼状图
---
```jsx
import { Pie, yuan } from 'ant-design-pro/lib/Charts';
const salesPieData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
ReactDOM.render(
<Pie
hasLegend
title="销售额"
subTitle="销售额"
total={() => (
<span
dangerouslySetInnerHTML={{
__html: yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))
}}
/>
)}
data={salesPieData}
valueFormat={val => <span dangerouslySetInnerHTML={{ __html: yuan(val) }} />}
height={294}
/>,
mountNode,
);
```

View File

@ -0,0 +1,64 @@
---
order: 7
title: 雷达图
---
````jsx
import { Radar, ChartCard } from 'ant-design-pro/lib/Charts';
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
const radarData = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach((item) => {
Object.keys(item).forEach((key) => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key],
value: item[key],
});
}
});
});
ReactDOM.render(
<ChartCard title="数据比例">
<Radar
hasLegend
height={286}
data={radarData}
/>
</ChartCard>
, mountNode);
````

View File

@ -0,0 +1,25 @@
---
order: 9
title: 标签云
---
标签云是一套相关的标签以及与此相应的权重展示方式,一般典型的标签云有 30 至 150 个标签,而权重影响使用的字体大小或其他视觉效果。
````jsx
import { TagCloud } from 'ant-design-pro/lib/Charts';
const tags = [];
for (let i = 0; i < 50; i += 1) {
tags.push({
name: `TagClout-Title-${i}`,
value: Math.floor((Math.random() * 50)) + 20,
});
}
ReactDOM.render(
<TagCloud
data={tags}
height={200}
/>
, mountNode);
````

View File

@ -0,0 +1,27 @@
---
order: 9
title: 带有时间轴的图表
---
使用 `TimelineChart` 组件可以实现带有时间轴的柱状图展现,而其中的 `x` 属性,则是时间值的指向,默认最多支持同时展现两个指标,分别是 `y1``y2`
````jsx
import { TimelineChart } from 'ant-design-pro/lib/Charts';
const chartData = [];
for (let i = 0; i < 20; i += 1) {
chartData.push({
x: (new Date().getTime()) + (1000 * 60 * 30 * i),
y1: Math.floor(Math.random() * 100) + 1000,
y2: Math.floor(Math.random() * 100) + 10,
});
}
ReactDOM.render(
<TimelineChart
height={200}
data={chartData}
titleMap={{ y1: '客流量', y2: '支付笔数' }}
/>
, mountNode);
````

View File

@ -0,0 +1,20 @@
---
order: 8
title: 水波图
---
水波图是一种比例的展示方式,可以更直观的展示关键值的占比。
````jsx
import { WaterWave } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
<div style={{ textAlign: 'center' }}>
<WaterWave
height={161}
title="补贴资金剩余"
percent={34}
/>
</div>
, mountNode);
````

View File

@ -0,0 +1,15 @@
// 全局 G2 设置
import { track, setTheme } from 'bizcharts';
track(false);
const config = {
defaultColor: '#1089ff',
shape: {
interval: {
fillOpacity: 1,
},
},
};
setTheme(config);

View File

@ -0,0 +1,17 @@
import * as numeral from 'numeral';
export { default as ChartCard } from './ChartCard';
export { default as Bar } from './Bar';
export { default as Pie } from './Pie';
export { default as Radar } from './Radar';
export { default as Gauge } from './Gauge';
export { default as MiniArea } from './MiniArea';
export { default as MiniBar } from './MiniBar';
export { default as MiniProgress } from './MiniProgress';
export { default as Field } from './Field';
export { default as WaterWave } from './WaterWave';
export { default as TagCloud } from './TagCloud';
export { default as TimelineChart } from './TimelineChart';
declare const yuan: (value: number | string) => string;
export { yuan };

View File

@ -0,0 +1,49 @@
import numeral from 'numeral';
import './g2';
import ChartCard from './ChartCard';
import Bar from './Bar';
import Pie from './Pie';
import Radar from './Radar';
import Gauge from './Gauge';
import MiniArea from './MiniArea';
import MiniBar from './MiniBar';
import MiniProgress from './MiniProgress';
import Field from './Field';
import WaterWave from './WaterWave';
import TagCloud from './TagCloud';
import TimelineChart from './TimelineChart';
const yuan = val => `¥ ${numeral(val).format('0,0')}`;
const Charts = {
yuan,
Bar,
Pie,
Gauge,
Radar,
MiniBar,
MiniArea,
MiniProgress,
ChartCard,
Field,
WaterWave,
TagCloud,
TimelineChart,
};
export {
Charts as default,
yuan,
Bar,
Pie,
Gauge,
Radar,
MiniBar,
MiniArea,
MiniProgress,
ChartCard,
Field,
WaterWave,
TagCloud,
TimelineChart,
};

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