初始化 antd-pro
This commit is contained in:
44
admin-web/src/app.js
Normal file
44
admin-web/src/app.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import fetch from 'dva/fetch';
|
||||
|
||||
export const dva = {
|
||||
config: {
|
||||
onError(err) {
|
||||
err.preventDefault();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let authRoutes = {};
|
||||
|
||||
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) {
|
||||
Object.keys(authRoutes).map(authKey =>
|
||||
ergodicRoutes(routes, authKey, authRoutes[authKey].authority)
|
||||
);
|
||||
window.g_routes = routes;
|
||||
}
|
||||
|
||||
export function render(oldRender) {
|
||||
fetch('/api/auth_routes')
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
ret => {
|
||||
authRoutes = ret;
|
||||
oldRender();
|
||||
},
|
||||
() => {
|
||||
oldRender();
|
||||
}
|
||||
);
|
||||
}
|
||||
43
admin-web/src/assets/logo.svg
Normal file
43
admin-web/src/assets/logo.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 28 Copy 5</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="62.1023273%" y1="0%" x2="108.19718%" y2="37.8635764%" id="linearGradient-1">
|
||||
<stop stop-color="#4285EB" offset="0%"></stop>
|
||||
<stop stop-color="#2EC7FF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="69.644116%" y1="0%" x2="54.0428975%" y2="108.456714%" id="linearGradient-2">
|
||||
<stop stop-color="#29CDFF" offset="0%"></stop>
|
||||
<stop stop-color="#148EFF" offset="37.8600687%"></stop>
|
||||
<stop stop-color="#0A60FF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="69.6908165%" y1="-12.9743587%" x2="16.7228981%" y2="117.391248%" id="linearGradient-3">
|
||||
<stop stop-color="#FA816E" offset="0%"></stop>
|
||||
<stop stop-color="#F74A5C" offset="41.472606%"></stop>
|
||||
<stop stop-color="#F51D2C" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-4">
|
||||
<stop stop-color="#FA8E7D" offset="0%"></stop>
|
||||
<stop stop-color="#F74A5C" offset="51.2635191%"></stop>
|
||||
<stop stop-color="#F51D2C" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="logo" transform="translate(-20.000000, -20.000000)">
|
||||
<g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)">
|
||||
<g id="Group-27-Copy-3">
|
||||
<g id="Group-25" fill-rule="nonzero">
|
||||
<g id="2">
|
||||
<path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-1)"></path>
|
||||
<path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-2)"></path>
|
||||
</g>
|
||||
<path d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z" id="Shape" fill="url(#linearGradient-3)"></path>
|
||||
</g>
|
||||
<ellipse id="Combined-Shape" fill="url(#linearGradient-4)" cx="100.519339" cy="100.436681" rx="23.6001926" ry="23.580786"></ellipse>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
98
admin-web/src/components/ActiveChart/index.js
Normal file
98
admin-web/src/components/ActiveChart/index.js
Normal file
@@ -0,0 +1,98 @@
|
||||
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.timer = setTimeout(() => {
|
||||
this.setState(
|
||||
{
|
||||
activeData: getActiveData(),
|
||||
},
|
||||
() => {
|
||||
this.loopData();
|
||||
}
|
||||
);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
51
admin-web/src/components/ActiveChart/index.less
Normal file
51
admin-web/src/components/ActiveChart/index.less
Normal 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;
|
||||
}
|
||||
17
admin-web/src/components/ArticleListContent/index.js
Normal file
17
admin-web/src/components/ArticleListContent/index.js
Normal 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;
|
||||
38
admin-web/src/components/ArticleListContent/index.less
Normal file
38
admin-web/src/components/ArticleListContent/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
admin-web/src/components/Authorized/Authorized.js
Normal file
8
admin-web/src/components/Authorized/Authorized.js
Normal 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;
|
||||
13
admin-web/src/components/Authorized/AuthorizedRoute.d.ts
vendored
Normal file
13
admin-web/src/components/Authorized/AuthorizedRoute.d.ts
vendored
Normal 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 default class AuthorizedRoute extends React.Component<IAuthorizedRouteProps, any> {}
|
||||
15
admin-web/src/components/Authorized/AuthorizedRoute.js
Normal file
15
admin-web/src/components/Authorized/AuthorizedRoute.js
Normal 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;
|
||||
88
admin-web/src/components/Authorized/CheckPermissions.js
Normal file
88
admin-web/src/components/Authorized/CheckPermissions.js
Normal 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;
|
||||
55
admin-web/src/components/Authorized/CheckPermissions.test.js
Normal file
55
admin-web/src/components/Authorized/CheckPermissions.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
65
admin-web/src/components/Authorized/PromiseRender.js
Normal file
65
admin-web/src/components/Authorized/PromiseRender.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
55
admin-web/src/components/Authorized/Secured.js
Normal file
55
admin-web/src/components/Authorized/Secured.js
Normal 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;
|
||||
23
admin-web/src/components/Authorized/demo/AuthorizedArray.md
Normal file
23
admin-web/src/components/Authorized/demo/AuthorizedArray.md
Normal 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,
|
||||
);
|
||||
```
|
||||
@@ -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,
|
||||
);
|
||||
```
|
||||
25
admin-web/src/components/Authorized/demo/basic.md
Normal file
25
admin-web/src/components/Authorized/demo/basic.md
Normal 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,
|
||||
);
|
||||
```
|
||||
28
admin-web/src/components/Authorized/demo/secured.md
Normal file
28
admin-web/src/components/Authorized/demo/secured.md
Normal 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
admin-web/src/components/Authorized/index.d.ts
vendored
Normal file
32
admin-web/src/components/Authorized/index.d.ts
vendored
Normal 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;
|
||||
11
admin-web/src/components/Authorized/index.js
Normal file
11
admin-web/src/components/Authorized/index.js
Normal 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);
|
||||
56
admin-web/src/components/Authorized/index.md
Normal file
56
admin-web/src/components/Authorized/index.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: 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 | - |
|
||||
25
admin-web/src/components/Authorized/renderAuthorize.js
Normal file
25
admin-web/src/components/Authorized/renderAuthorize.js
Normal 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);
|
||||
10
admin-web/src/components/AvatarList/AvatarItem.d.ts
vendored
Normal file
10
admin-web/src/components/AvatarList/AvatarItem.d.ts
vendored
Normal 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);
|
||||
}
|
||||
24
admin-web/src/components/AvatarList/demo/maxLength.md
Normal file
24
admin-web/src/components/AvatarList/demo/maxLength.md
Normal 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);
|
||||
````
|
||||
20
admin-web/src/components/AvatarList/demo/simple.md
Normal file
20
admin-web/src/components/AvatarList/demo/simple.md
Normal 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
admin-web/src/components/AvatarList/index.d.ts
vendored
Normal file
14
admin-web/src/components/AvatarList/index.d.ts
vendored
Normal 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;
|
||||
}
|
||||
24
admin-web/src/components/AvatarList/index.en-US.md
Normal file
24
admin-web/src/components/AvatarList/index.en-US.md
Normal 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 | - |
|
||||
61
admin-web/src/components/AvatarList/index.js
Normal file
61
admin-web/src/components/AvatarList/index.js
Normal 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;
|
||||
50
admin-web/src/components/AvatarList/index.less
Normal file
50
admin-web/src/components/AvatarList/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
admin-web/src/components/AvatarList/index.test.js
Normal file
29
admin-web/src/components/AvatarList/index.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
25
admin-web/src/components/AvatarList/index.zh-CN.md
Normal file
25
admin-web/src/components/AvatarList/index.zh-CN.md
Normal 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 | - |
|
||||
44
admin-web/src/components/Charts/AsyncLoadBizCharts.js
Normal file
44
admin-web/src/components/Charts/AsyncLoadBizCharts.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import PageLoading from '../PageLoading';
|
||||
import { importCDN } from '@/utils/utils';
|
||||
|
||||
let isLoaderBizChart = false;
|
||||
const loadBizCharts = async () => {
|
||||
if (isLoaderBizChart) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
await Promise.all([
|
||||
importCDN('//gw.alipayobjects.com/os/lib/bizcharts/3.4.3/umd/BizCharts.min.js'),
|
||||
importCDN('//gw.alipayobjects.com/os/lib/antv/data-set/0.10.1/dist/data-set.min.js'),
|
||||
]);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('bizCharts load success');
|
||||
isLoaderBizChart = true;
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
class AsyncLoadBizCharts extends React.Component {
|
||||
state = {
|
||||
loading: !isLoaderBizChart,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
await loadBizCharts();
|
||||
requestAnimationFrame(() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { loading } = this.state;
|
||||
if (!loading) {
|
||||
return children;
|
||||
}
|
||||
return <PageLoading />;
|
||||
}
|
||||
}
|
||||
|
||||
export { loadBizCharts, AsyncLoadBizCharts };
|
||||
15
admin-web/src/components/Charts/Bar/index.d.ts
vendored
Normal file
15
admin-web/src/components/Charts/Bar/index.d.ts
vendored
Normal 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> {}
|
||||
113
admin-web/src/components/Charts/Bar/index.js
Normal file
113
admin-web/src/components/Charts/Bar/index.js
Normal 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;
|
||||
14
admin-web/src/components/Charts/ChartCard/index.d.ts
vendored
Normal file
14
admin-web/src/components/Charts/ChartCard/index.d.ts
vendored
Normal 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> {}
|
||||
82
admin-web/src/components/Charts/ChartCard/index.js
Normal file
82
admin-web/src/components/Charts/ChartCard/index.js
Normal 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;
|
||||
75
admin-web/src/components/Charts/ChartCard/index.less
Normal file
75
admin-web/src/components/Charts/ChartCard/index.less
Normal file
@@ -0,0 +1,75 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.chartCard {
|
||||
position: relative;
|
||||
.chartTop {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.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 {
|
||||
height: 22px;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
line-height: 22px;
|
||||
}
|
||||
.action {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 0;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.total {
|
||||
height: 38px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
color: @heading-color;
|
||||
font-size: 30px;
|
||||
line-height: 38px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.contentFixed {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 8px;
|
||||
padding-top: 9px;
|
||||
border-top: 1px solid @border-color-split;
|
||||
& > * {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.footerMargin {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
8
admin-web/src/components/Charts/Field/index.d.ts
vendored
Normal file
8
admin-web/src/components/Charts/Field/index.d.ts
vendored
Normal 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> {}
|
||||
12
admin-web/src/components/Charts/Field/index.js
Normal file
12
admin-web/src/components/Charts/Field/index.js
Normal 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;
|
||||
17
admin-web/src/components/Charts/Field/index.less
Normal file
17
admin-web/src/components/Charts/Field/index.less
Normal 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
admin-web/src/components/Charts/Gauge/index.d.ts
vendored
Normal file
11
admin-web/src/components/Charts/Gauge/index.d.ts
vendored
Normal 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> {}
|
||||
167
admin-web/src/components/Charts/Gauge/index.js
Normal file
167
admin-web/src/components/Charts/Gauge/index.js
Normal 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;
|
||||
29
admin-web/src/components/Charts/MiniArea/index.d.ts
vendored
Normal file
29
admin-web/src/components/Charts/MiniArea/index.d.ts
vendored
Normal 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> {}
|
||||
108
admin-web/src/components/Charts/MiniArea/index.js
Normal file
108
admin-web/src/components/Charts/MiniArea/index.js
Normal 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;
|
||||
12
admin-web/src/components/Charts/MiniBar/index.d.ts
vendored
Normal file
12
admin-web/src/components/Charts/MiniBar/index.d.ts
vendored
Normal 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> {}
|
||||
51
admin-web/src/components/Charts/MiniBar/index.js
Normal file
51
admin-web/src/components/Charts/MiniBar/index.js
Normal 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;
|
||||
10
admin-web/src/components/Charts/MiniProgress/index.d.ts
vendored
Normal file
10
admin-web/src/components/Charts/MiniProgress/index.d.ts
vendored
Normal 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> {}
|
||||
27
admin-web/src/components/Charts/MiniProgress/index.js
Normal file
27
admin-web/src/components/Charts/MiniProgress/index.js
Normal 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;
|
||||
35
admin-web/src/components/Charts/MiniProgress/index.less
Normal file
35
admin-web/src/components/Charts/MiniProgress/index.less
Normal 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
admin-web/src/components/Charts/Pie/index.d.ts
vendored
Normal file
21
admin-web/src/components/Charts/Pie/index.d.ts
vendored
Normal 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> {}
|
||||
271
admin-web/src/components/Charts/Pie/index.js
Normal file
271
admin-web/src/components/Charts/Pie/index.js
Normal 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;
|
||||
94
admin-web/src/components/Charts/Pie/index.less
Normal file
94
admin-web/src/components/Charts/Pie/index.less
Normal 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
admin-web/src/components/Charts/Radar/index.d.ts
vendored
Normal file
15
admin-web/src/components/Charts/Radar/index.d.ts
vendored
Normal 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> {}
|
||||
184
admin-web/src/components/Charts/Radar/index.js
Normal file
184
admin-web/src/components/Charts/Radar/index.js
Normal 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;
|
||||
46
admin-web/src/components/Charts/Radar/index.less
Normal file
46
admin-web/src/components/Charts/Radar/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
admin-web/src/components/Charts/TagCloud/index.d.ts
vendored
Normal file
11
admin-web/src/components/Charts/TagCloud/index.d.ts
vendored
Normal 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> {}
|
||||
182
admin-web/src/components/Charts/TagCloud/index.js
Normal file
182
admin-web/src/components/Charts/TagCloud/index.js
Normal 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;
|
||||
6
admin-web/src/components/Charts/TagCloud/index.less
Normal file
6
admin-web/src/components/Charts/TagCloud/index.less
Normal file
@@ -0,0 +1,6 @@
|
||||
.tagCloud {
|
||||
overflow: hidden;
|
||||
canvas {
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
}
|
||||
14
admin-web/src/components/Charts/TimelineChart/index.d.ts
vendored
Normal file
14
admin-web/src/components/Charts/TimelineChart/index.d.ts
vendored
Normal 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> {}
|
||||
120
admin-web/src/components/Charts/TimelineChart/index.js
Normal file
120
admin-web/src/components/Charts/TimelineChart/index.js
Normal 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;
|
||||
3
admin-web/src/components/Charts/TimelineChart/index.less
Normal file
3
admin-web/src/components/Charts/TimelineChart/index.less
Normal file
@@ -0,0 +1,3 @@
|
||||
.timelineChart {
|
||||
background: #fff;
|
||||
}
|
||||
10
admin-web/src/components/Charts/WaterWave/index.d.ts
vendored
Normal file
10
admin-web/src/components/Charts/WaterWave/index.d.ts
vendored
Normal 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> {}
|
||||
213
admin-web/src/components/Charts/WaterWave/index.js
Normal file
213
admin-web/src/components/Charts/WaterWave/index.js
Normal 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;
|
||||
28
admin-web/src/components/Charts/WaterWave/index.less
Normal file
28
admin-web/src/components/Charts/WaterWave/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
62
admin-web/src/components/Charts/autoHeight.js
Normal file
62
admin-web/src/components/Charts/autoHeight.js
Normal 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
admin-web/src/components/Charts/bizcharts.d.ts
vendored
Normal file
3
admin-web/src/components/Charts/bizcharts.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as BizChart from 'bizcharts';
|
||||
|
||||
export = BizChart;
|
||||
3
admin-web/src/components/Charts/bizcharts.js
Normal file
3
admin-web/src/components/Charts/bizcharts.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as BizChart from 'bizcharts';
|
||||
|
||||
export default BizChart;
|
||||
26
admin-web/src/components/Charts/demo/bar.md
Normal file
26
admin-web/src/components/Charts/demo/bar.md
Normal 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);
|
||||
````
|
||||
95
admin-web/src/components/Charts/demo/chart-card.md
Normal file
95
admin-web/src/components/Charts/demo/chart-card.md
Normal 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,
|
||||
);
|
||||
```
|
||||
18
admin-web/src/components/Charts/demo/gauge.md
Normal file
18
admin-web/src/components/Charts/demo/gauge.md
Normal 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);
|
||||
````
|
||||
28
admin-web/src/components/Charts/demo/mini-area.md
Normal file
28
admin-web/src/components/Charts/demo/mini-area.md
Normal 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);
|
||||
````
|
||||
28
admin-web/src/components/Charts/demo/mini-bar.md
Normal file
28
admin-web/src/components/Charts/demo/mini-bar.md
Normal 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);
|
||||
````
|
||||
16
admin-web/src/components/Charts/demo/mini-pie.md
Normal file
16
admin-web/src/components/Charts/demo/mini-pie.md
Normal 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
|
||||
);
|
||||
```
|
||||
12
admin-web/src/components/Charts/demo/mini-progress.md
Normal file
12
admin-web/src/components/Charts/demo/mini-progress.md
Normal 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);
|
||||
````
|
||||
84
admin-web/src/components/Charts/demo/mix.md
Normal file
84
admin-web/src/components/Charts/demo/mix.md
Normal 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);
|
||||
````
|
||||
54
admin-web/src/components/Charts/demo/pie.md
Normal file
54
admin-web/src/components/Charts/demo/pie.md
Normal 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,
|
||||
);
|
||||
```
|
||||
64
admin-web/src/components/Charts/demo/radar.md
Normal file
64
admin-web/src/components/Charts/demo/radar.md
Normal 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);
|
||||
````
|
||||
25
admin-web/src/components/Charts/demo/tag-cloud.md
Normal file
25
admin-web/src/components/Charts/demo/tag-cloud.md
Normal 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);
|
||||
````
|
||||
27
admin-web/src/components/Charts/demo/timeline-chart.md
Normal file
27
admin-web/src/components/Charts/demo/timeline-chart.md
Normal 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);
|
||||
````
|
||||
20
admin-web/src/components/Charts/demo/waterwave.md
Normal file
20
admin-web/src/components/Charts/demo/waterwave.md
Normal 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);
|
||||
````
|
||||
48
admin-web/src/components/Charts/index.d.ts
vendored
Normal file
48
admin-web/src/components/Charts/index.d.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as numeral from 'numeral';
|
||||
import { default as Bar } from './Bar';
|
||||
import { default as ChartCard } from './ChartCard';
|
||||
import { default as Field } from './Field';
|
||||
import { default as Gauge } from './Gauge';
|
||||
import { default as MiniArea } from './MiniArea';
|
||||
import { default as MiniBar } from './MiniBar';
|
||||
import { default as MiniProgress } from './MiniProgress';
|
||||
import { default as Pie } from './Pie';
|
||||
import { default as Radar } from './Radar';
|
||||
import { default as TagCloud } from './TagCloud';
|
||||
import { default as TimelineChart } from './TimelineChart';
|
||||
import { default as WaterWave } from './WaterWave';
|
||||
|
||||
declare const yuan: (value: number | string) => string;
|
||||
|
||||
declare const Charts: {
|
||||
yuan: (value: number | string) => string;
|
||||
Bar: Bar;
|
||||
Pie: Pie;
|
||||
Gauge: Gauge;
|
||||
Radar: Radar;
|
||||
MiniBar: MiniBar;
|
||||
MiniArea: MiniArea;
|
||||
MiniProgress: MiniProgress;
|
||||
ChartCard: ChartCard;
|
||||
Field: Field;
|
||||
WaterWave: WaterWave;
|
||||
TagCloud: TagCloud;
|
||||
TimelineChart: TimelineChart;
|
||||
};
|
||||
|
||||
export {
|
||||
Charts as default,
|
||||
yuan,
|
||||
Bar,
|
||||
Pie,
|
||||
Gauge,
|
||||
Radar,
|
||||
MiniBar,
|
||||
MiniArea,
|
||||
MiniProgress,
|
||||
ChartCard,
|
||||
Field,
|
||||
WaterWave,
|
||||
TagCloud,
|
||||
TimelineChart,
|
||||
};
|
||||
60
admin-web/src/components/Charts/index.js
Normal file
60
admin-web/src/components/Charts/index.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import numeral from 'numeral';
|
||||
import ChartCard from './ChartCard';
|
||||
import Field from './Field';
|
||||
|
||||
const getComponent = Component => {
|
||||
return props => {
|
||||
return (
|
||||
<Suspense fallback="...">
|
||||
<Component {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Bar = getComponent(React.lazy(() => import('./Bar')));
|
||||
const Pie = getComponent(React.lazy(() => import('./Pie')));
|
||||
const Radar = getComponent(React.lazy(() => import('./Radar')));
|
||||
const Gauge = getComponent(React.lazy(() => import('./Gauge')));
|
||||
const MiniArea = getComponent(React.lazy(() => import('./MiniArea')));
|
||||
const MiniBar = getComponent(React.lazy(() => import('./MiniBar')));
|
||||
const MiniProgress = getComponent(React.lazy(() => import('./MiniProgress')));
|
||||
const WaterWave = getComponent(React.lazy(() => import('./WaterWave')));
|
||||
const TagCloud = getComponent(React.lazy(() => import('./TagCloud')));
|
||||
const TimelineChart = getComponent(React.lazy(() => import('./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,
|
||||
};
|
||||
19
admin-web/src/components/Charts/index.less
Normal file
19
admin-web/src/components/Charts/index.less
Normal file
@@ -0,0 +1,19 @@
|
||||
.miniChart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.chartContent {
|
||||
position: absolute;
|
||||
bottom: -28px;
|
||||
width: 100%;
|
||||
> div {
|
||||
margin: 0 -5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.chartLoading {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
margin-left: -7px;
|
||||
}
|
||||
}
|
||||
130
admin-web/src/components/Charts/index.md
Normal file
130
admin-web/src/components/Charts/index.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: Charts
|
||||
subtitle: 图表
|
||||
order: 2
|
||||
cols: 2
|
||||
---
|
||||
|
||||
Ant Design Pro 提供的业务中常用的图表类型,都是基于 [G2](https://antv.alipay.com/g2/doc/index.html) 按照 Ant Design 图表规范封装,需要注意的是 Ant Design Pro 的图表组件以套件形式提供,可以任意组合实现复杂的业务需求。
|
||||
|
||||
因为结合了 Ant Design 的标准设计,本着极简的设计思想以及开箱即用的理念,简化了大量 API 配置,所以如果需要灵活定制图表,可以参考 Ant Design Pro 图表实现,自行基于 [G2](https://antv.alipay.com/g2/doc/index.html) 封装图表组件使用。
|
||||
|
||||
## API
|
||||
|
||||
### ChartCard
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 卡片标题 | ReactNode\|string | - |
|
||||
| action | 卡片操作 | ReactNode | - |
|
||||
| total | 数据总量 | ReactNode \| number \| function | - |
|
||||
| footer | 卡片底部 | ReactNode | - |
|
||||
| contentHeight | 内容区域高度 | number | - |
|
||||
| avatar | 右侧图标 | React.ReactNode | - |
|
||||
### MiniBar
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| color | 图表颜色 | string | `#1890FF` |
|
||||
| height | 图表高度 | number | - |
|
||||
| data | 数据 | array<{x, y}> | - |
|
||||
|
||||
### MiniArea
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.2)` |
|
||||
| borderColor | 图表边颜色 | string | `#1890FF` |
|
||||
| height | 图表高度 | number | - |
|
||||
| line | 是否显示描边 | boolean | false |
|
||||
| animate | 是否显示动画 | boolean | true |
|
||||
| xAxis | [x 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
|
||||
| yAxis | [y 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
|
||||
| data | 数据 | array<{x, y}> | - |
|
||||
|
||||
### MiniProgress
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| target | 目标比例 | number | - |
|
||||
| color | 进度条颜色 | string | - |
|
||||
| strokeWidth | 进度条高度 | number | - |
|
||||
| percent | 进度比例 | number | - |
|
||||
|
||||
### Bar
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
|
||||
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
|
||||
| height | 图表高度 | number | - |
|
||||
| data | 数据 | array<{x, y}> | - |
|
||||
| autoLabel | 在宽度不足时,自动隐藏 x 轴的 label | boolean | `true` |
|
||||
|
||||
### Pie
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| animate | 是否显示动画 | boolean | true |
|
||||
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
|
||||
| height | 图表高度 | number | - |
|
||||
| hasLegend | 是否显示 legend | boolean | `false` |
|
||||
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
|
||||
| percent | 占比 | number | - |
|
||||
| tooltip | 是否显示 tooltip | boolean | true |
|
||||
| valueFormat | 显示值的格式化函数 | function | - |
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| subTitle | 图表子标题 | ReactNode\|string | - |
|
||||
| total | 图标中央的总数 | string | function | - |
|
||||
|
||||
### Radar
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| height | 图表高度 | number | - |
|
||||
| hasLegend | 是否显示 legend | boolean | `false` |
|
||||
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
|
||||
| data | 图标数据 | array<{name,label,value}> | - |
|
||||
|
||||
### Gauge
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| height | 图表高度 | number | - |
|
||||
| color | 图表颜色 | string | `#2F9CFF` |
|
||||
| bgColor | 图表背景颜色 | string | `#F0F2F5` |
|
||||
| percent | 进度比例 | number | - |
|
||||
|
||||
### WaterWave
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| height | 图表高度 | number | - |
|
||||
| color | 图表颜色 | string | `#1890FF` |
|
||||
| percent | 进度比例 | number | - |
|
||||
|
||||
### TagCloud
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| data | 标题 | Array<name, value\> | - |
|
||||
| height | 高度值 | number | - |
|
||||
|
||||
### TimelineChart
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| data | 标题 | Array<x, y1, y2\> | - |
|
||||
| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - |
|
||||
| height | 高度值 | number | 400 |
|
||||
|
||||
### Field
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| label | 标题 | ReactNode\|string | - |
|
||||
| value | 值 | ReactNode\|string | - |
|
||||
24
admin-web/src/components/CountDown/demo/simple.md
Normal file
24
admin-web/src/components/CountDown/demo/simple.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
order: 0
|
||||
title:
|
||||
zh-CN: 基本
|
||||
en-US: Basic
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
简单的倒计时组件使用。
|
||||
|
||||
## en-US
|
||||
|
||||
The simplest usage.
|
||||
|
||||
````jsx
|
||||
import CountDown from 'ant-design-pro/lib/CountDown';
|
||||
|
||||
const targetTime = new Date().getTime() + 3900000;
|
||||
|
||||
ReactDOM.render(
|
||||
<CountDown style={{ fontSize: 20 }} target={targetTime} />
|
||||
, mountNode);
|
||||
````
|
||||
9
admin-web/src/components/CountDown/index.d.ts
vendored
Normal file
9
admin-web/src/components/CountDown/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
export interface ICountDownProps {
|
||||
format?: (time: number) => void;
|
||||
target: Date | number;
|
||||
onEnd?: () => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class CountDown extends React.Component<ICountDownProps, any> {}
|
||||
15
admin-web/src/components/CountDown/index.en-US.md
Normal file
15
admin-web/src/components/CountDown/index.en-US.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: CountDown
|
||||
cols: 1
|
||||
order: 3
|
||||
---
|
||||
|
||||
Simple CountDown Component.
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| format | Formatter of time | Function(time) | |
|
||||
| target | Target time | Date | - |
|
||||
| onEnd | Countdown to the end callback | funtion | -|
|
||||
121
admin-web/src/components/CountDown/index.js
Normal file
121
admin-web/src/components/CountDown/index.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
function fixedZero(val) {
|
||||
return val * 1 < 10 ? `0${val}` : val;
|
||||
}
|
||||
const initTime = props => {
|
||||
let lastTime = 0;
|
||||
let targetTime = 0;
|
||||
try {
|
||||
if (Object.prototype.toString.call(props.target) === '[object Date]') {
|
||||
targetTime = props.target.getTime();
|
||||
} else {
|
||||
targetTime = new Date(props.target).getTime();
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error('invalid target prop', e);
|
||||
}
|
||||
|
||||
lastTime = targetTime - new Date().getTime();
|
||||
return {
|
||||
lastTime: lastTime < 0 ? 0 : lastTime,
|
||||
};
|
||||
};
|
||||
|
||||
class CountDown extends Component {
|
||||
timer = 0;
|
||||
|
||||
interval = 1000;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { lastTime } = initTime(props);
|
||||
this.state = {
|
||||
lastTime,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, preState) {
|
||||
const { lastTime } = initTime(nextProps);
|
||||
if (preState.lastTime !== lastTime) {
|
||||
return {
|
||||
lastTime,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.tick();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { target } = this.props;
|
||||
if (target !== prevProps.target) {
|
||||
clearTimeout(this.timer);
|
||||
this.tick();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
// defaultFormat = time => (
|
||||
// <span>{moment(time).format('hh:mm:ss')}</span>
|
||||
// );
|
||||
defaultFormat = time => {
|
||||
const hours = 60 * 60 * 1000;
|
||||
const minutes = 60 * 1000;
|
||||
|
||||
const h = Math.floor(time / hours);
|
||||
const m = Math.floor((time - h * hours) / minutes);
|
||||
const s = Math.floor((time - h * hours - m * minutes) / 1000);
|
||||
return (
|
||||
<span>
|
||||
{fixedZero(h)}:{fixedZero(m)}:{fixedZero(s)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
tick = () => {
|
||||
const { onEnd } = this.props;
|
||||
let { lastTime } = this.state;
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
if (lastTime < this.interval) {
|
||||
clearTimeout(this.timer);
|
||||
this.setState(
|
||||
{
|
||||
lastTime: 0,
|
||||
},
|
||||
() => {
|
||||
if (onEnd) {
|
||||
onEnd();
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
lastTime -= this.interval;
|
||||
this.setState(
|
||||
{
|
||||
lastTime,
|
||||
},
|
||||
() => {
|
||||
this.tick();
|
||||
}
|
||||
);
|
||||
}
|
||||
}, this.interval);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { format = this.defaultFormat, onEnd, ...rest } = this.props;
|
||||
const { lastTime } = this.state;
|
||||
const result = format(lastTime);
|
||||
|
||||
return <span {...rest}>{result}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export default CountDown;
|
||||
16
admin-web/src/components/CountDown/index.zh-CN.md
Normal file
16
admin-web/src/components/CountDown/index.zh-CN.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: CountDown
|
||||
subtitle: 倒计时
|
||||
cols: 1
|
||||
order: 3
|
||||
---
|
||||
|
||||
倒计时组件。
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| format | 时间格式化显示 | Function(time) | |
|
||||
| target | 目标时间 | Date | - |
|
||||
| onEnd | 倒计时结束回调 | funtion | -|
|
||||
9
admin-web/src/components/DescriptionList/Description.d.ts
vendored
Normal file
9
admin-web/src/components/DescriptionList/Description.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default class Description extends React.Component<
|
||||
{
|
||||
term: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
},
|
||||
any
|
||||
> {}
|
||||
22
admin-web/src/components/DescriptionList/Description.js
Normal file
22
admin-web/src/components/DescriptionList/Description.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col } from 'antd';
|
||||
import styles from './index.less';
|
||||
import responsive from './responsive';
|
||||
|
||||
const Description = ({ term, column, children, ...restProps }) => (
|
||||
<Col {...responsive[column]} {...restProps}>
|
||||
{term && <div className={styles.term}>{term}</div>}
|
||||
{children !== null && children !== undefined && <div className={styles.detail}>{children}</div>}
|
||||
</Col>
|
||||
);
|
||||
|
||||
Description.defaultProps = {
|
||||
term: '',
|
||||
};
|
||||
|
||||
Description.propTypes = {
|
||||
term: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Description;
|
||||
33
admin-web/src/components/DescriptionList/DescriptionList.js
Normal file
33
admin-web/src/components/DescriptionList/DescriptionList.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Row } from 'antd';
|
||||
import styles from './index.less';
|
||||
|
||||
const DescriptionList = ({
|
||||
className,
|
||||
title,
|
||||
col = 3,
|
||||
layout = 'horizontal',
|
||||
gutter = 32,
|
||||
children,
|
||||
size,
|
||||
...restProps
|
||||
}) => {
|
||||
const clsString = classNames(styles.descriptionList, styles[layout], className, {
|
||||
[styles.small]: size === 'small',
|
||||
[styles.large]: size === 'large',
|
||||
});
|
||||
const column = col > 4 ? 4 : col;
|
||||
return (
|
||||
<div className={clsString} {...restProps}>
|
||||
{title ? <div className={styles.title}>{title}</div> : null}
|
||||
<Row gutter={gutter}>
|
||||
{React.Children.map(children, child =>
|
||||
child ? React.cloneElement(child, { column }) : child
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionList;
|
||||
43
admin-web/src/components/DescriptionList/demo/basic.md
Normal file
43
admin-web/src/components/DescriptionList/demo/basic.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
order: 0
|
||||
title:
|
||||
zh-CN: 基本
|
||||
en-US: Basic
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
基本描述列表。
|
||||
|
||||
## en-US
|
||||
|
||||
Basic DescriptionList.
|
||||
|
||||
````jsx
|
||||
import DescriptionList from 'ant-design-pro/lib/DescriptionList';
|
||||
|
||||
const { Description } = DescriptionList;
|
||||
|
||||
ReactDOM.render(
|
||||
<DescriptionList size="large" title="title">
|
||||
<Description term="Firefox">
|
||||
A free, open source, cross-platform,
|
||||
graphical web browser developed by the
|
||||
Mozilla Corporation and hundreds of
|
||||
volunteers.
|
||||
</Description>
|
||||
<Description term="Firefox">
|
||||
A free, open source, cross-platform,
|
||||
graphical web browser developed by the
|
||||
Mozilla Corporation and hundreds of
|
||||
volunteers.
|
||||
</Description>
|
||||
<Description term="Firefox">
|
||||
A free, open source, cross-platform,
|
||||
graphical web browser developed by the
|
||||
Mozilla Corporation and hundreds of
|
||||
volunteers.
|
||||
</Description>
|
||||
</DescriptionList>
|
||||
, mountNode);
|
||||
````
|
||||
43
admin-web/src/components/DescriptionList/demo/vertical.md
Normal file
43
admin-web/src/components/DescriptionList/demo/vertical.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
order: 1
|
||||
title:
|
||||
zh-CN: 垂直型
|
||||
en-US: Vertical
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
垂直布局。
|
||||
|
||||
## en-US
|
||||
|
||||
Vertical layout.
|
||||
|
||||
````jsx
|
||||
import DescriptionList from 'ant-design-pro/lib/DescriptionList';
|
||||
|
||||
const { Description } = DescriptionList;
|
||||
|
||||
ReactDOM.render(
|
||||
<DescriptionList size="large" title="title" layout="vertical">
|
||||
<Description term="Firefox">
|
||||
A free, open source, cross-platform,
|
||||
graphical web browser developed by the
|
||||
Mozilla Corporation and hundreds of
|
||||
volunteers.
|
||||
</Description>
|
||||
<Description term="Firefox">
|
||||
A free, open source, cross-platform,
|
||||
graphical web browser developed by the
|
||||
Mozilla Corporation and hundreds of
|
||||
volunteers.
|
||||
</Description>
|
||||
<Description term="Firefox">
|
||||
A free, open source, cross-platform,
|
||||
graphical web browser developed by the
|
||||
Mozilla Corporation and hundreds of
|
||||
volunteers.
|
||||
</Description>
|
||||
</DescriptionList>
|
||||
, mountNode);
|
||||
````
|
||||
15
admin-web/src/components/DescriptionList/index.d.ts
vendored
Normal file
15
admin-web/src/components/DescriptionList/index.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import Description from './Description';
|
||||
|
||||
export interface IDescriptionListProps {
|
||||
layout?: 'horizontal' | 'vertical';
|
||||
col?: number;
|
||||
title: React.ReactNode;
|
||||
gutter?: number;
|
||||
size?: 'large' | 'small';
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class DescriptionList extends React.Component<IDescriptionListProps, any> {
|
||||
public static Description: typeof Description;
|
||||
}
|
||||
33
admin-web/src/components/DescriptionList/index.en-US.md
Normal file
33
admin-web/src/components/DescriptionList/index.en-US.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: DescriptionList
|
||||
cols: 1
|
||||
order: 4
|
||||
---
|
||||
|
||||
Groups display multiple read-only fields, which are common to informational displays on detail pages.
|
||||
|
||||
## API
|
||||
|
||||
### DescriptionList
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|----------|------------------------------------------|-------------|---------|
|
||||
| layout | type of layout | Enum{'horizontal', 'vertical'} | 'horizontal' |
|
||||
| col | specify the maximum number of columns to display, the final columns number is determined by col setting combined with [Responsive Rules](/components/DescriptionList#Responsive-Rules) | number(0 < col <= 4) | 3 |
|
||||
| title | title | ReactNode | - |
|
||||
| gutter | specify the distance between two items, unit is `px` | number | 32 |
|
||||
| size | size of list | Enum{'large', 'small'} | - |
|
||||
|
||||
#### Responsive Rules
|
||||
|
||||
| Window Width | Columns Number |
|
||||
|---------------------|---------------------------------------------|
|
||||
| `≥768px` | `col` |
|
||||
| `≥576px` | `col < 2 ? col : 2` |
|
||||
| `<576px` | `1` |
|
||||
|
||||
### DescriptionList.Description
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| term | item title | ReactNode | - |
|
||||
5
admin-web/src/components/DescriptionList/index.js
Normal file
5
admin-web/src/components/DescriptionList/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import DescriptionList from './DescriptionList';
|
||||
import Description from './Description';
|
||||
|
||||
DescriptionList.Description = Description;
|
||||
export default DescriptionList;
|
||||
76
admin-web/src/components/DescriptionList/index.less
Normal file
76
admin-web/src/components/DescriptionList/index.less
Normal file
@@ -0,0 +1,76 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.descriptionList {
|
||||
// offset the padding-bottom of last row
|
||||
:global {
|
||||
.ant-row {
|
||||
margin-bottom: -16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
color: @heading-color;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.term {
|
||||
display: table-cell;
|
||||
padding-bottom: 16px;
|
||||
color: @heading-color;
|
||||
// Line-height is 22px IE dom height will calculate error
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: -0.5px;
|
||||
margin: 0 8px 0 2px;
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
color: @text-color;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.small {
|
||||
// offset the padding-bottom of last row
|
||||
:global {
|
||||
.ant-row {
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 12px;
|
||||
color: @text-color;
|
||||
}
|
||||
.term,
|
||||
.detail {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
.term {
|
||||
display: block;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
admin-web/src/components/DescriptionList/index.zh-CN.md
Normal file
37
admin-web/src/components/DescriptionList/index.zh-CN.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: DescriptionList
|
||||
subtitle: 描述列表
|
||||
cols: 1
|
||||
order: 4
|
||||
---
|
||||
|
||||
成组展示多个只读字段,常见于详情页的信息展示。
|
||||
|
||||
## API
|
||||
|
||||
### DescriptionList
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| layout | 布局方式 | Enum{'horizontal', 'vertical'} | 'horizontal' |
|
||||
| col | 指定信息最多分几列展示,最终一行几列由 col 配置结合[响应式规则](/components/DescriptionList#响应式规则)决定 | number(0 < col <= 4) | 3 |
|
||||
| title | 列表标题 | ReactNode | - |
|
||||
| gutter | 列表项间距,单位为 `px` | number | 32 |
|
||||
| size | 列表型号 | Enum{'large', 'small'} | - |
|
||||
|
||||
#### 响应式规则
|
||||
|
||||
| 窗口宽度 | 展示列数 |
|
||||
|---------------------|---------------------------------------------|
|
||||
| `≥768px` | `col` |
|
||||
| `≥576px` | `col < 2 ? col : 2` |
|
||||
| `<576px` | `1` |
|
||||
|
||||
### DescriptionList.Description
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| term | 列表项标题 | ReactNode | - |
|
||||
|
||||
|
||||
|
||||
6
admin-web/src/components/DescriptionList/responsive.js
Normal file
6
admin-web/src/components/DescriptionList/responsive.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
1: { xs: 24 },
|
||||
2: { xs: 24, sm: 12 },
|
||||
3: { xs: 24, sm: 12, md: 8 },
|
||||
4: { xs: 24, sm: 12, md: 6 },
|
||||
};
|
||||
50
admin-web/src/components/EditableItem/index.js
Normal file
50
admin-web/src/components/EditableItem/index.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Input, Icon } from 'antd';
|
||||
import styles from './index.less';
|
||||
|
||||
export default class EditableItem extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: props.value,
|
||||
editable: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
const { value } = e.target;
|
||||
this.setState({ value });
|
||||
};
|
||||
|
||||
check = () => {
|
||||
this.setState({ editable: false });
|
||||
const { value } = this.state;
|
||||
const { onChange } = this.props;
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
edit = () => {
|
||||
this.setState({ editable: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, editable } = this.state;
|
||||
return (
|
||||
<div className={styles.editableItem}>
|
||||
{editable ? (
|
||||
<div className={styles.wrapper}>
|
||||
<Input value={value} onChange={this.handleChange} onPressEnter={this.check} />
|
||||
<Icon type="check" className={styles.icon} onClick={this.check} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.wrapper}>
|
||||
<span>{value || ' '}</span>
|
||||
<Icon type="edit" className={styles.icon} onClick={this.edit} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user