🎉 2.6.1发布,增加登陆验证码,支持Seata1.0

This commit is contained in:
smallchill 2020-02-12 01:02:23 +08:00
parent 2bae7753bd
commit e380417953
467 changed files with 30448 additions and 0 deletions

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

3
scripts/generateMock.js Normal file
View File

@ -0,0 +1,3 @@
const generateMock = require('merge-umi-mock-data');
const path = require('path');
generateMock(path.join(__dirname, '../mock'), path.join(__dirname, '../functions/mock/index.js'));

View File

@ -0,0 +1,23 @@
const glob = require('glob');
const getPrettierFiles = () => {
let files = [];
const configFiles = glob.sync('config/**/*.js*', { ignore: ['**/node_modules/**', 'build/**'] });
const mockFiles = glob.sync('mock/**/*.js*', { ignore: ['**/node_modules/**', 'build/**'] });
const jsFiles = glob.sync('src/**/*.js*', { ignore: ['**/node_modules/**', 'build/**'] });
const scriptFiles = glob.sync('scripts/**/*.js');
const tsFiles = glob.sync('src/**/*.ts*', { ignore: ['**/node_modules/**', 'build/**'] });
const lessFiles = glob.sync('src/**/*.less*', { ignore: ['**/node_modules/**', 'build/**'] });
files = files.concat(configFiles);
files = files.concat(mockFiles);
files = files.concat(jsFiles);
files = files.concat(scriptFiles);
files = files.concat(tsFiles);
files = files.concat(lessFiles);
if (!files.length) {
return;
}
return files;
};
module.exports = getPrettierFiles;

50
scripts/lint-prettier.js Normal file
View File

@ -0,0 +1,50 @@
/**
* copy to https://github.com/facebook/react/blob/master/scripts/prettier/index.js
* prettier api doc https://prettier.io/docs/en/api.html
*----------*****--------------
* lint file is prettier
*----------*****--------------
*/
const prettier = require('prettier');
const fs = require('fs');
const chalk = require('chalk');
const prettierConfigPath = require.resolve('../.prettierrc');
const files = process.argv.slice(2);
let didError = false;
files.forEach(file => {
Promise.all([
prettier.resolveConfig(file, {
config: prettierConfigPath,
}),
prettier.getFileInfo(file),
])
.then(resolves => {
const [options, fileInfo] = resolves;
if (fileInfo.ignored) {
return;
}
const input = fs.readFileSync(file, 'utf8');
const withParserOptions = {
...options,
parser: fileInfo.inferredParser,
};
const output = prettier.format(input, withParserOptions);
if (output !== input) {
fs.writeFileSync(file, output, 'utf8');
console.log(chalk.green(`${file} is prettier`));
}
})
.catch(e => {
didError = true;
})
.finally(() => {
if (didError) {
process.exit(1);
}
console.log(chalk.hex('#1890FF')('prettier success!'));
});
});

46
scripts/prettier.js Normal file
View File

@ -0,0 +1,46 @@
/**
* copy to https://github.com/facebook/react/blob/master/scripts/prettier/index.js
* prettier api doc https://prettier.io/docs/en/api.html
*----------*****--------------
* prettier all js and all ts.
*----------*****--------------
*/
const prettier = require('prettier');
const fs = require('fs');
const getPrettierFiles = require('./getPrettierFiles');
const prettierConfigPath = require.resolve('../.prettierrc');
const chalk = require('chalk');
let didError = false;
const files = getPrettierFiles();
files.forEach(file => {
const options = prettier.resolveConfig.sync(file, {
config: prettierConfigPath,
});
const fileInfo = prettier.getFileInfo.sync(file);
if (fileInfo.ignored) {
return;
}
try {
const input = fs.readFileSync(file, 'utf8');
const withParserOptions = {
...options,
parser: fileInfo.inferredParser,
};
const output = prettier.format(input, withParserOptions);
if (output !== input) {
fs.writeFileSync(file, output, 'utf8');
console.log(chalk.green(`${file} is prettier`));
}
} catch (e) {
didError = true;
}
});
if (didError) {
process.exit(1);
}
console.log(chalk.hex('#1890FF')('prettier success!'));

36
src/actions/client.js Normal file
View File

@ -0,0 +1,36 @@
export const CLIENT_NAMESPACE = 'client';
export function CLIENT_LIST(payload) {
return {
type: `${CLIENT_NAMESPACE}/fetchList`,
payload,
};
}
export function CLIENT_DETAIL(id) {
return {
type: `${CLIENT_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function CLIENT_CLEAR_DETAIL() {
return {
type: `${CLIENT_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function CLIENT_SUBMIT(payload) {
return {
type: `${CLIENT_NAMESPACE}/submit`,
payload,
};
}
export function CLIENT_REMOVE(payload) {
return {
type: `${CLIENT_NAMESPACE}/remove`,
payload,
};
}

43
src/actions/code.js Normal file
View File

@ -0,0 +1,43 @@
export const CODE_NAMESPACE = 'code';
export function CODE_LIST(payload) {
return {
type: `${CODE_NAMESPACE}/fetchList`,
payload,
};
}
export function CODE_INIT() {
return {
type: `${CODE_NAMESPACE}/fetchInit`,
payload: { code: 'yes_no' },
};
}
export function CODE_DETAIL(id) {
return {
type: `${CODE_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function CODE_CLEAR_DETAIL() {
return {
type: `${CODE_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function CODE_SUBMIT(payload) {
return {
type: `${CODE_NAMESPACE}/submit`,
payload,
};
}
export function CODE_REMOVE(payload) {
return {
type: `${CODE_NAMESPACE}/remove`,
payload,
};
}

36
src/actions/datasource.js Normal file
View File

@ -0,0 +1,36 @@
export const DATASOURCE_NAMESPACE = 'datasource';
export function DATASOURCE_LIST(payload) {
return {
type: `${DATASOURCE_NAMESPACE}/fetchList`,
payload,
};
}
export function DATASOURCE_DETAIL(id) {
return {
type: `${DATASOURCE_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function DATASOURCE_CLEAR_DETAIL() {
return {
type: `${DATASOURCE_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function DATASOURCE_SUBMIT(payload) {
return {
type: `${DATASOURCE_NAMESPACE}/submit`,
payload,
};
}
export function DATASOURCE_REMOVE(payload) {
return {
type: `${DATASOURCE_NAMESPACE}/remove`,
payload,
};
}

43
src/actions/dept.js Normal file
View File

@ -0,0 +1,43 @@
export const DEPT_NAMESPACE = 'dept';
export function DEPT_LIST(payload) {
return {
type: `${DEPT_NAMESPACE}/fetchList`,
payload,
};
}
export function DEPT_INIT() {
return {
type: `${DEPT_NAMESPACE}/fetchInit`,
payload: {},
};
}
export function DEPT_DETAIL(id) {
return {
type: `${DEPT_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function DEPT_CLEAR_DETAIL() {
return {
type: `${DEPT_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function DEPT_SUBMIT(payload) {
return {
type: `${DEPT_NAMESPACE}/submit`,
payload,
};
}
export function DEPT_REMOVE(payload) {
return {
type: `${DEPT_NAMESPACE}/remove`,
payload,
};
}

43
src/actions/dict.js Normal file
View File

@ -0,0 +1,43 @@
export const DICT_NAMESPACE = 'dict';
export function DICT_LIST(payload) {
return {
type: `${DICT_NAMESPACE}/fetchList`,
payload,
};
}
export function DICT_INIT() {
return {
type: `${DICT_NAMESPACE}/fetchInit`,
payload: { code: 'DICT' },
};
}
export function DICT_DETAIL(id) {
return {
type: `${DICT_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function DICT_CLEAR_DETAIL() {
return {
type: `${DICT_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function DICT_SUBMIT(payload) {
return {
type: `${DICT_NAMESPACE}/submit`,
payload,
};
}
export function DICT_REMOVE(payload) {
return {
type: `${DICT_NAMESPACE}/remove`,
payload,
};
}

43
src/actions/log.js Normal file
View File

@ -0,0 +1,43 @@
export const LOG_NAMESPACE = 'log';
export function LOG_USUAL_LIST(payload) {
return {
type: `${LOG_NAMESPACE}/fetchUsualList`,
payload,
};
}
export function LOG_USUAL_DETAIL(id) {
return {
type: `${LOG_NAMESPACE}/fetchUsualDetail`,
payload: { id },
};
}
export function LOG_API_LIST(payload) {
return {
type: `${LOG_NAMESPACE}/fetchApiList`,
payload,
};
}
export function LOG_API_DETAIL(id) {
return {
type: `${LOG_NAMESPACE}/fetchApiDetail`,
payload: { id },
};
}
export function LOG_ERROR_LIST(payload) {
return {
type: `${LOG_NAMESPACE}/fetchErrorList`,
payload,
};
}
export function LOG_ERROR_DETAIL(id) {
return {
type: `${LOG_NAMESPACE}/fetchErrorDetail`,
payload: { id },
};
}

61
src/actions/menu.js Normal file
View File

@ -0,0 +1,61 @@
import { getAuthority } from '../utils/authority';
export const MENU_NAMESPACE = 'menu';
export function MENU_REFRESH_DATA() {
return {
type: `${MENU_NAMESPACE}/fetchMenuData`,
payload: { authority: getAuthority() },
};
}
export function MENU_LIST(payload) {
return {
type: `${MENU_NAMESPACE}/fetchList`,
payload,
};
}
export function MENU_INIT() {
return {
type: `${MENU_NAMESPACE}/fetchInit`,
payload: {},
};
}
export function MENU_DETAIL(id) {
return {
type: `${MENU_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function MENU_CLEAR_DETAIL() {
return {
type: `${MENU_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function MENU_SUBMIT(payload) {
return {
type: `${MENU_NAMESPACE}/submit`,
payload,
};
}
export function MENU_REMOVE(payload) {
return {
type: `${MENU_NAMESPACE}/remove`,
payload,
};
}
export function MENU_SELECT_ICON(icon) {
return {
type: `${MENU_NAMESPACE}/selectIcon`,
payload: {
source: icon,
},
};
}

36
src/actions/notice.js Normal file
View File

@ -0,0 +1,36 @@
export const NOTICE_NAMESPACE = 'notice';
export function NOTICE_LIST(payload) {
return {
type: `${NOTICE_NAMESPACE}/fetchList`,
payload,
};
}
export function NOTICE_INIT() {
return {
type: `${NOTICE_NAMESPACE}/fetchInit`,
payload: { code: 'notice' },
};
}
export function NOTICE_DETAIL(id) {
return {
type: `${NOTICE_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function NOTICE_SUBMIT(payload) {
return {
type: `${NOTICE_NAMESPACE}/submit`,
payload,
};
}
export function NOTICE_REMOVE(payload) {
return {
type: `${NOTICE_NAMESPACE}/remove`,
payload,
};
}

36
src/actions/param.js Normal file
View File

@ -0,0 +1,36 @@
export const PARAM_NAMESPACE = 'param';
export function PARAM_LIST(payload) {
return {
type: `${PARAM_NAMESPACE}/fetchList`,
payload,
};
}
export function PARAM_DETAIL(id) {
return {
type: `${PARAM_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function PARAM_CLEAR_DETAIL() {
return {
type: `${PARAM_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function PARAM_SUBMIT(payload) {
return {
type: `${PARAM_NAMESPACE}/submit`,
payload,
};
}
export function PARAM_REMOVE(payload) {
return {
type: `${PARAM_NAMESPACE}/remove`,
payload,
};
}

72
src/actions/role.js Normal file
View File

@ -0,0 +1,72 @@
export const ROLE_NAMESPACE = 'role';
export function ROLE_LIST(payload) {
return {
type: `${ROLE_NAMESPACE}/fetchList`,
payload,
};
}
export function ROLE_INIT() {
return {
type: `${ROLE_NAMESPACE}/fetchInit`,
payload: {},
};
}
export function ROLE_DETAIL(id) {
return {
type: `${ROLE_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function ROLE_CLEAR_DETAIL() {
return {
type: `${ROLE_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function ROLE_GRANT_TREE(payload) {
return {
type: `${ROLE_NAMESPACE}/grantTree`,
payload,
};
}
export function ROLE_TREE_KEYS(payload) {
return {
type: `${ROLE_NAMESPACE}/roleTreeKeys`,
payload,
};
}
export function ROLE_SET_TREE_KEYS(payload) {
return {
type: `${ROLE_NAMESPACE}/setRoleTreeKeys`,
payload,
};
}
export function ROLE_GRANT(payload, callback) {
return {
type: `${ROLE_NAMESPACE}/grant`,
payload,
callback,
};
}
export function ROLE_SUBMIT(payload) {
return {
type: `${ROLE_NAMESPACE}/submit`,
payload,
};
}
export function ROLE_REMOVE(payload) {
return {
type: `${ROLE_NAMESPACE}/remove`,
payload,
};
}

36
src/actions/tenant.js Normal file
View File

@ -0,0 +1,36 @@
export const TENANT_NAMESPACE = 'tenant';
export function TENANT_LIST(payload) {
return {
type: `${TENANT_NAMESPACE}/fetchList`,
payload,
};
}
export function TENANT_DETAIL(id) {
return {
type: `${TENANT_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function TENANT_CLEAR_DETAIL() {
return {
type: `${TENANT_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function TENANT_SUBMIT(payload) {
return {
type: `${TENANT_NAMESPACE}/submit`,
payload,
};
}
export function TENANT_REMOVE(payload) {
return {
type: `${TENANT_NAMESPACE}/remove`,
payload,
};
}

58
src/actions/user.js Normal file
View File

@ -0,0 +1,58 @@
export const USER_NAMESPACE = 'user';
export function USER_LIST(payload) {
return {
type: `${USER_NAMESPACE}/fetchList`,
payload,
};
}
export function USER_INIT() {
return {
type: `${USER_NAMESPACE}/fetchInit`,
payload: {},
};
}
export function USER_CHANGE_INIT(payload) {
return {
type: `${USER_NAMESPACE}/fetchChangeInit`,
payload,
};
}
export function USER_DETAIL(id) {
return {
type: `${USER_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function USER_ROLE_GRANT(payload, callback) {
return {
type: `${USER_NAMESPACE}/grant`,
payload,
callback,
};
}
export function USER_SUBMIT(payload) {
return {
type: `${USER_NAMESPACE}/submit`,
payload,
};
}
export function USER_UPDATE(payload) {
return {
type: `${USER_NAMESPACE}/update`,
payload,
};
}
export function USER_REMOVE(payload) {
return {
type: `${USER_NAMESPACE}/remove`,
payload,
};
}

43
src/app.js Normal file
View File

@ -0,0 +1,43 @@
import { routesAuthority } from './services/menu';
export const dva = {
config: {
onError(err) {
err.preventDefault();
},
},
};
let authRoutes = {
'/form/advanced-form': { authority: ['admin', 'user'] },
};
function ergodicRoutes(routes, authKey, authority) {
routes.forEach(element => {
if (element.path === authKey) {
if (!element.authority) element.authority = []; // eslint-disable-line
Object.assign(element.authority, authority || []);
} else if (element.routes) {
ergodicRoutes(element.routes, authKey, authority);
}
return element;
});
}
export function patchRoutes(routes) {
if (authRoutes !== null && authRoutes !== undefined) {
Object.keys(authRoutes).map(authKey =>
ergodicRoutes(routes, authKey, authRoutes[authKey].authority)
);
window.g_routes = routes;
}
}
export function render(oldRender) {
routesAuthority().then(response => {
if (response && response.success) {
authRoutes = response.data;
}
oldRender();
});
}

160
src/assets/logo.svg Normal file
View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="87px" height="86px" viewBox="0 0 87 86" enable-background="new 0 0 87 86" xml:space="preserve"> <image id="image0" width="87" height="86" x="0" y="0"
xlink:href="data:image/png;base64,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
MzErMDg6MDC8c1XdAAAAAElFTkSuQmCC" />
</svg>

View File

@ -0,0 +1,101 @@
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>
<div className={styles.activeChartGrid}>
<p>{[...activeData].sort()[activeData.length - 1].y + 200} 亿元</p>
<p>{[...activeData].sort()[Math.floor(activeData.length / 2)].y} 亿元</p>
</div>
<div className={styles.dashedLine}>
<div className={styles.line} />
</div>
<div className={styles.dashedLine}>
<div className={styles.line} />
</div>
</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,51 @@
.activeChart {
position: relative;
}
.activeChartGrid {
p {
position: absolute;
top: 80px;
}
p:last-child {
top: 115px;
}
}
.activeChartLegend {
position: relative;
height: 20px;
margin-top: 8px;
font-size: 0;
line-height: 20px;
span {
display: inline-block;
width: 33.33%;
font-size: 12px;
text-align: center;
}
span:first-child {
text-align: left;
}
span:last-child {
text-align: right;
}
}
.dashedLine {
position: relative;
top: -70px;
left: -3px;
height: 1px;
.line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(to right, transparent 50%, #e9e9e9 50%);
background-size: 6px;
}
}
.dashedLine:last-child {
top: -36px;
}

View File

@ -0,0 +1,54 @@
import React from 'react';
export default ({
expendAll = false,
expandedRowRender,
updateExpandRowKeys,
initExpandedRowKeys,
}) => {
return WrappedComponent => {
return class extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
expandedRowKeys: initExpandedRowKeys || [],
expandRowByClick: false,
};
}
componentDidMount() {
if (initExpandedRowKeys) {
this.onExpandedRowsChange(initExpandedRowKeys);
}
}
onExpandedRowsChange = rows => {
if (updateExpandRowKeys) updateExpandRowKeys(rows);
this.setState({
expandedRowKeys: rows,
});
};
expandRow = row => {
this.setState({
expandedRowKeys: row ? [row] : [],
});
};
render() {
const { expandedRowKeys } = this.state;
// const expandRowByClick = true;
return (
<WrappedComponent
expandedRowRender={expandedRowRender}
defaultExpandAllRows={expendAll}
expandedRowKeys={expandedRowKeys}
onExpandedRowsChange={this.onExpandedRowsChange}
expandRow={this.expandRow}
/>
);
}
};
};
};

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 {
max-width: 720px;
line-height: 22px;
}
.extra {
margin-top: 16px;
color: @text-color-secondary;
line-height: 22px;
& > :global(.ant-avatar) {
position: relative;
top: 1px;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: top;
}
& > em {
margin-left: 16px;
color: @disabled-color;
font-style: normal;
}
}
}
@media screen and (max-width: @screen-xs) {
.listContent {
.extra {
& > em {
display: block;
margin-top: 8px;
margin-left: 0;
}
}
}
}

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,13 @@
import * as React from 'react';
import { RouteProps } from 'react-router';
type authorityFN = (currentAuthority?: string) => boolean;
type authority = string | string[] | authorityFN | Promise<any>;
export interface IAuthorizedRouteProps extends RouteProps {
authority: authority;
}
export { authority };
export class AuthorizedRoute extends React.Component<IAuthorizedRouteProps, any> {}

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,
);
```

32
src/components/Authorized/index.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
import * as React from 'react';
import AuthorizedRoute, { authority } from './AuthorizedRoute';
export type IReactComponent<P = any> =
| React.StatelessComponent<P>
| React.ComponentClass<P>
| React.ClassicComponentClass<P>;
type Secured = (
authority: authority,
error?: React.ReactNode
) => <T extends IReactComponent>(target: T) => T;
type check = <T extends IReactComponent, S extends IReactComponent>(
authority: authority,
target: T,
Exception: S
) => T | S;
export interface IAuthorizedProps {
authority: authority;
noMatch?: React.ReactNode;
}
export class Authorized extends React.Component<IAuthorizedProps, any> {
public static Secured: Secured;
public static AuthorizedRoute: typeof AuthorizedRoute;
public 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,24 @@
---
order: 0
title:
zh-CN: 要显示的最大项目
en-US: Max Items to Show
---
`maxLength` attribute specifies the maximum number of items to show while `excessItemsStyle` style the excess
item component.
````jsx
import AvatarList from 'ant-design-pro/lib/AvatarList';
ReactDOM.render(
<AvatarList size="mini" maxLength={3} excessItemsStyle={{ color: '#f56a00', backgroundColor: '#fde3cf' }}>
<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.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<AvatarList.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<AvatarList.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</AvatarList>
, mountNode);
````

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);
````

14
src/components/AvatarList/index.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import * as React from 'react';
import AvatarItem from './AvatarItem';
export interface IAvatarListProps {
size?: 'large' | 'small' | 'mini' | 'default';
maxLength?: number;
excessItemsStyle?: React.CSSProperties;
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,24 @@
---
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` |
| maxLength | max items to show | number | - |
| excessItemsStyle | the excess item style | CSSProperties | - |
### 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,61 @@
import React from 'react';
import { Tooltip, Avatar } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
const avatarSizeToClassName = size =>
classNames(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',
});
const AvatarList = ({ children, size, maxLength, excessItemsStyle, ...other }) => {
const numOfChildren = React.Children.count(children);
const numToShow = maxLength >= numOfChildren ? numOfChildren : maxLength;
const childrenWithProps = React.Children.toArray(children)
.slice(0, numToShow)
.map(child =>
React.cloneElement(child, {
size,
})
);
if (numToShow < numOfChildren) {
const cls = avatarSizeToClassName(size);
childrenWithProps.push(
<li key="exceed" className={cls}>
<Avatar size={size} style={excessItemsStyle}>{`+${numOfChildren - maxLength}`}</Avatar>
</li>
);
}
return (
<div {...other} className={styles.avatarList}>
<ul> {childrenWithProps} </ul>
</div>
);
};
const Item = ({ src, size, tips, onClick = () => {} }) => {
const cls = avatarSizeToClassName(size);
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,50 @@
@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;
width: @avatar-size-base;
height: @avatar-size-base;
margin-left: -8px;
font-size: @font-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;
.ant-avatar-string {
font-size: 12px;
line-height: 18px;
}
}
}
}

View File

@ -0,0 +1,29 @@
import React from 'react';
import range from 'lodash/range';
import { mount } from 'enzyme';
import AvatarList from './index';
const renderItems = numItems =>
range(numItems).map(i => (
<AvatarList.Item
key={i}
tips="Jake"
src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png"
/>
));
describe('AvatarList', () => {
it('renders all items', () => {
const wrapper = mount(<AvatarList>{renderItems(4)}</AvatarList>);
expect(wrapper.find('AvatarList').length).toBe(1);
expect(wrapper.find('Item').length).toBe(4);
expect(wrapper.findWhere(node => node.key() === 'exceed').length).toBe(0);
});
it('renders max of 3 items', () => {
const wrapper = mount(<AvatarList maxLength={3}>{renderItems(4)}</AvatarList>);
expect(wrapper.find('AvatarList').length).toBe(1);
expect(wrapper.find('Item').length).toBe(3);
expect(wrapper.findWhere(node => node.key() === 'exceed').length).toBe(1);
});
});

View File

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

15
src/components/Charts/Bar/index.d.ts vendored Normal file
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 { CardProps } from 'antd/lib/card';
import * as React from 'react';
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,75 @@
@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: 4px;
right: 0;
line-height: 1;
}
.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 className={styles.label}>{label}</span>
<span className={styles.number}>{value}</span>
</div>
);
export default Field;

View File

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

11
src/components/Charts/Gauge/index.d.ts vendored Normal file
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 {
position: relative;
width: 100%;
padding: 5px 0;
.progressWrap {
position: relative;
background-color: @background-color-base;
}
.progress {
width: 0;
height: 100%;
background-color: @primary-color;
border-radius: 1px 0 0 1px;
transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
}
.target {
position: absolute;
top: 0;
bottom: 0;
span {
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 4px;
border-radius: 100px;
}
span:last-child {
top: auto;
bottom: 0;
}
}
}

21
src/components/Charts/Pie/index.d.ts vendored Normal file
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;
top: 50%;
right: 0;
min-width: 200px;
margin: 0 20px;
padding: 0;
list-style: none;
transform: translateY(-50%);
li {
height: 22px;
margin-bottom: 16px;
line-height: 22px;
cursor: pointer;
&:last-child {
margin-bottom: 0;
}
}
}
.dot {
position: relative;
top: -1px;
display: inline-block;
width: 8px;
height: 8px;
margin-right: 8px;
border-radius: 8px;
}
.line {
display: inline-block;
width: 1px;
height: 16px;
margin-right: 8px;
background-color: @border-color-split;
}
.legendTitle {
color: @text-color;
}
.percent {
color: @text-color-secondary;
}
.value {
position: absolute;
right: 0;
}
.title {
margin-bottom: 8px;
}
.total {
position: absolute;
top: 50%;
left: 50%;
max-height: 62px;
text-align: center;
transform: translate(-50%, -50%);
& > h4 {
height: 22px;
margin-bottom: 8px;
color: @text-color-secondary;
font-weight: normal;
font-size: 14px;
line-height: 22px;
}
& > p {
display: block;
height: 32px;
color: @heading-color;
font-size: 1.2em;
line-height: 32px;
white-space: nowrap;
}
}
}
.legendBlock {
&.hasLegend .chart {
width: 100%;
margin: 0 0 32px 0;
}
.legend {
position: relative;
transform: none;
}
}

15
src/components/Charts/Radar/index.d.ts vendored Normal file
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 = 5,
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;
color: @text-color-secondary;
line-height: 22px;
text-align: center;
cursor: pointer;
p {
margin: 0;
}
h6 {
margin-top: 4px;
margin-bottom: 0;
padding-left: 16px;
color: @heading-color;
font-size: 24px;
line-height: 32px;
}
&::after {
position: absolute;
top: 8px;
right: 0;
width: 1px;
height: 40px;
background-color: @border-color-split;
content: '';
}
}
> :last-child .legendItem::after {
display: none;
}
.dot {
position: relative;
top: -1px;
display: inline-block;
width: 6px;
height: 6px;
margin-right: 6px;
border-radius: 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,182 @@
import React, { Component } from 'react';
import { Chart, Geom, Coord, Shape, Tooltip } 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;
const w = this.root.offsetWidth;
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: 0,
timeInterval: 5000, // max execute time
rotate() {
return 0;
},
fontSize(d) {
// eslint-disable-next-line
return Math.pow((d.value - min) / (max - min), 2) * (17.5 - 5) + 5;
},
});
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 },
}}
>
<Tooltip showTitle={false} />
<Coord reflect="y" />
<Geom
type="point"
position="x*y"
color="text"
shape="cloud"
tooltip={[
'text*value',
function trans(text, value) {
return { name: text, value };
},
]}
/>
</Chart>
)}
</div>
);
}
}
export default TagCloud;

View File

@ -0,0 +1,6 @@
.tagCloud {
overflow: hidden;
canvas {
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,120 @@
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: sourceData,
} = this.props;
const data = Array.isArray(sourceData) ? sourceData : [{ x: 0, y1: 0, y2: 0 }];
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 {
position: relative;
display: inline-block;
transform-origin: left;
.text {
position: absolute;
top: 32px;
left: 0;
width: 100%;
text-align: center;
span {
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
h4 {
color: @heading-color;
font-size: 24px;
line-height: 32px;
}
}
.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;

3
src/components/Charts/bizcharts.d.ts vendored Normal file
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);

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