mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 02:59:58 +08:00
New component Statistic / Countdown (#14154)
* init
* number format
* add Countdown
* ⚡ Try something ts stuff
* move out Countdown
* add format
* support countdown
* add prefix & suffix
* Move Number to Statistic
* adjust ts
* clean up
* roll back of lodash
* update doc & style
* variable of the less style
* add demo test
* full coverage
* hide title if not need
* update snapshot
* update accessiblity
* update color
* stop countdown when time is out
* formatTimeStr adjust
* use reset class
* add miss tab index
* update doc
* update title prop & snapshot
* rm additional aria. It's over design
* use card sample on unit demo
* sfc
* adjust demo
This commit is contained in:
parent
54463c908b
commit
ff13ac0f5e
@ -35,6 +35,7 @@ Array [
|
||||
"message",
|
||||
"Menu",
|
||||
"Modal",
|
||||
"Statistic",
|
||||
"notification",
|
||||
"Pagination",
|
||||
"Popconfirm",
|
||||
|
@ -11043,6 +11043,66 @@ exports[`ConfigProvider components Spin prefixCls 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components Statistic configProvider 1`] = `
|
||||
<div
|
||||
class="config-statistic"
|
||||
>
|
||||
<div
|
||||
class="config-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="config-statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="config-statistic-content-value-int"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components Statistic normal 1`] = `
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value-int"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components Statistic prefixCls 1`] = `
|
||||
<div
|
||||
class="prefix-Statistic"
|
||||
>
|
||||
<div
|
||||
class="prefix-Statistic-content"
|
||||
>
|
||||
<span
|
||||
class="prefix-Statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="prefix-Statistic-content-value-int"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components Steps configProvider 1`] = `
|
||||
<div
|
||||
class="config-steps config-steps-horizontal config-steps-label-horizontal"
|
||||
|
@ -40,6 +40,7 @@ import Select from '../../select';
|
||||
import Skeleton from '../../skeleton';
|
||||
import Slider from '../../slider';
|
||||
import Spin from '../../spin';
|
||||
import Statistic from '../../statistic';
|
||||
import Steps from '../../steps';
|
||||
import Switch from '../../switch';
|
||||
import Table from '../../table';
|
||||
@ -427,6 +428,9 @@ describe('ConfigProvider', () => {
|
||||
// Spin
|
||||
testPair('Spin', props => <Spin {...props} />);
|
||||
|
||||
// Statistic
|
||||
testPair('Statistic', props => <Statistic {...props} value={0} />);
|
||||
|
||||
// Steps
|
||||
testPair('Steps', props => {
|
||||
const myProps = { ...props };
|
||||
|
@ -87,9 +87,9 @@ interface ConsumerConfig {
|
||||
}
|
||||
|
||||
export function withConfigConsumer<ExportProps extends BasicExportProps>(config: ConsumerConfig) {
|
||||
return function(Component: IReactComponent): React.SFC<ExportProps> {
|
||||
return function <ComponentDef>(Component: IReactComponent): React.SFC<ExportProps> & ComponentDef {
|
||||
// Wrap with ConfigConsumer. Since we need compatible with react 15, be care when using ref methods
|
||||
return (props: ExportProps) => (
|
||||
return ((props: ExportProps) => (
|
||||
<ConfigConsumer>
|
||||
{(configProps: ConfigConsumerProps) => {
|
||||
const { prefixCls: basicPrefixCls } = config;
|
||||
@ -99,7 +99,7 @@ export function withConfigConsumer<ExportProps extends BasicExportProps>(config:
|
||||
return <Component {...configProps} {...props} prefixCls={prefixCls} />;
|
||||
}}
|
||||
</ConfigConsumer>
|
||||
);
|
||||
)) as React.SFC<ExportProps> & ComponentDef;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -81,6 +81,8 @@ export { default as Menu } from './menu';
|
||||
|
||||
export { default as Modal } from './modal';
|
||||
|
||||
export { default as Statistic } from './statistic';
|
||||
|
||||
export { default as notification } from './notification';
|
||||
|
||||
export { default as Pagination } from './pagination';
|
||||
|
78
components/statistic/Countdown.tsx
Normal file
78
components/statistic/Countdown.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import * as React from 'react';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
import * as moment from 'moment';
|
||||
import interopDefault from '../_util/interopDefault';
|
||||
import Statistic, { StatisticProps } from './Statistic';
|
||||
import { formatCountdown, countdownValueType, FormatConfig } from './utils';
|
||||
|
||||
const REFRESH_INTERVAL = 1000 / 30;
|
||||
|
||||
interface CountdownProps extends StatisticProps {
|
||||
value?: countdownValueType;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
class Countdown extends React.Component<CountdownProps, {}> {
|
||||
static defaultProps: Partial<CountdownProps> = {
|
||||
format: 'HH:mm:ss',
|
||||
};
|
||||
|
||||
countdownId?: number = undefined;
|
||||
|
||||
componentDidMount() {
|
||||
this.syncTimer();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.syncTimer();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
}
|
||||
|
||||
syncTimer = () => {
|
||||
const { value } = this.props;
|
||||
|
||||
const timestamp = interopDefault(moment)(value).valueOf();
|
||||
if (timestamp >= Date.now()) {
|
||||
this.startTimer();
|
||||
} else {
|
||||
this.stopTimer();
|
||||
}
|
||||
};
|
||||
|
||||
startTimer = () => {
|
||||
if (this.countdownId !== undefined) return;
|
||||
|
||||
this.countdownId = window.setInterval(() => {
|
||||
this.forceUpdate();
|
||||
}, REFRESH_INTERVAL);
|
||||
};
|
||||
|
||||
stopTimer = () => {
|
||||
clearInterval(this.countdownId);
|
||||
this.countdownId = undefined;
|
||||
};
|
||||
|
||||
formatCountdown = (value: countdownValueType, config: FormatConfig) => {
|
||||
const { format } = this.props;
|
||||
return formatCountdown(value, { ...config, format });
|
||||
};
|
||||
|
||||
// Countdown do not need display the timestamp
|
||||
valueRender = (node: React.ReactElement<HTMLDivElement>) =>
|
||||
React.cloneElement(node, {
|
||||
title: undefined,
|
||||
});
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Statistic valueRender={this.valueRender} {...this.props} formatter={this.formatCountdown} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
polyfill(Countdown);
|
||||
|
||||
export default Countdown;
|
55
components/statistic/Number.tsx
Normal file
55
components/statistic/Number.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import padEnd from 'lodash/padEnd';
|
||||
import { valueType, FormatConfig } from './utils';
|
||||
|
||||
interface NumberProps extends FormatConfig {
|
||||
value: valueType;
|
||||
}
|
||||
|
||||
const StatisticNumber: React.SFC<NumberProps> = props => {
|
||||
const { value, formatter, precision, decimalSeparator, prefixCls } = props;
|
||||
|
||||
let valueNode: React.ReactNode;
|
||||
|
||||
if (typeof formatter === 'function') {
|
||||
// Customize formatter
|
||||
valueNode = formatter(value);
|
||||
} else {
|
||||
// Internal formatter
|
||||
const val: string = String(value);
|
||||
const cells = val.match(/^(\d*)(\.(\d+))?$/);
|
||||
|
||||
// Process if illegal number
|
||||
if (!cells) {
|
||||
valueNode = val;
|
||||
} else {
|
||||
let int = cells[1] || '0';
|
||||
let decimal = cells[3] || '';
|
||||
|
||||
int = int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
|
||||
if (typeof precision === 'number') {
|
||||
decimal = padEnd(decimal, precision, '0').slice(0, precision);
|
||||
}
|
||||
|
||||
if (decimal) {
|
||||
decimal = `${decimalSeparator}${decimal}`;
|
||||
}
|
||||
|
||||
valueNode = [
|
||||
<span key="int" className={`${prefixCls}-content-value-int`}>
|
||||
{int}
|
||||
</span>,
|
||||
decimal && (
|
||||
<span key="decimal" className={`${prefixCls}-content-value-decimal`}>
|
||||
{decimal}
|
||||
</span>
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return <span className={`${prefixCls}-content-value`}>{valueNode}</span>;
|
||||
};
|
||||
|
||||
export default StatisticNumber;
|
64
components/statistic/Statistic.tsx
Normal file
64
components/statistic/Statistic.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { withConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import StatisticNumber from './Number';
|
||||
import Countdown from './Countdown';
|
||||
import { valueType, FormatConfig } from './utils';
|
||||
|
||||
interface StatisticComponent {
|
||||
Countdown: typeof Countdown;
|
||||
}
|
||||
|
||||
export interface StatisticProps extends FormatConfig {
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
value?: valueType;
|
||||
valueStyle?: React.CSSProperties;
|
||||
valueRender?: (node: React.ReactNode) => React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
prefix?: React.ReactNode;
|
||||
suffix?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Statistic: React.SFC<StatisticProps & ConfigConsumerProps> = props => {
|
||||
const {
|
||||
prefixCls,
|
||||
className,
|
||||
style,
|
||||
valueStyle,
|
||||
value = 0,
|
||||
title,
|
||||
valueRender,
|
||||
prefix,
|
||||
suffix,
|
||||
} = props;
|
||||
|
||||
let valueNode: React.ReactNode = <StatisticNumber {...props} value={value} />;
|
||||
|
||||
if (valueRender) {
|
||||
valueNode = valueRender(valueNode);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(prefixCls, className)} style={style}>
|
||||
{title && <div className={`${prefixCls}-title`}>{title}</div>}
|
||||
<div style={valueStyle} className={`${prefixCls}-content`}>
|
||||
{prefix && <span className={`${prefixCls}-content-prefix`}>{prefix}</span>}
|
||||
{valueNode}
|
||||
{suffix && <span className={`${prefixCls}-content-suffix`}>{suffix}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Statistic.defaultProps = {
|
||||
decimalSeparator: '.',
|
||||
};
|
||||
|
||||
const WrapperStatistic = withConfigConsumer<StatisticProps>({
|
||||
prefixCls: 'statistic',
|
||||
})<StatisticComponent>(Statistic);
|
||||
|
||||
export default WrapperStatistic;
|
388
components/statistic/__tests__/__snapshots__/demo.test.js.snap
Normal file
388
components/statistic/__tests__/__snapshots__/demo.test.js.snap
Normal file
@ -0,0 +1,388 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders ./components/statistic/demo/basic.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-row"
|
||||
style="margin-left:-8px;margin-right:-8px"
|
||||
>
|
||||
<div
|
||||
class="ant-col-12"
|
||||
style="padding-left:8px;padding-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Active Users
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value-int"
|
||||
>
|
||||
112,893
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col-12"
|
||||
style="padding-left:8px;padding-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Account Balance (CNY)
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value-int"
|
||||
>
|
||||
112,893
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-value-decimal"
|
||||
>
|
||||
.00
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="ant-btn ant-btn-primary"
|
||||
style="margin-top:16px"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Recharge
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/statistic/demo/card.md correctly 1`] = `
|
||||
<div
|
||||
style="background:#ECECEC;padding:30px"
|
||||
>
|
||||
<div
|
||||
class="ant-row"
|
||||
style="margin-left:-8px;margin-right:-8px"
|
||||
>
|
||||
<div
|
||||
class="ant-col-12"
|
||||
style="padding-left:8px;padding-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-card ant-card-bordered"
|
||||
>
|
||||
<div
|
||||
class="ant-card-body"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Active
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
style="color:#3f8600"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-prefix"
|
||||
>
|
||||
<i
|
||||
class="anticon anticon-arrow-up"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="arrow-up"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M868 545.5L536.1 163a31.96 31.96 0 0 0-48.3 0L156 545.5a7.97 7.97 0 0 0 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value-int"
|
||||
>
|
||||
11
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-value-decimal"
|
||||
>
|
||||
.28
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-suffix"
|
||||
>
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col-12"
|
||||
style="padding-left:8px;padding-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-card ant-card-bordered"
|
||||
>
|
||||
<div
|
||||
class="ant-card-body"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Idle
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
style="color:#cf1322"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-prefix"
|
||||
>
|
||||
<i
|
||||
class="anticon anticon-arrow-down"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="arrow-down"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0 0 48.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value-int"
|
||||
>
|
||||
9
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-value-decimal"
|
||||
>
|
||||
.30
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-suffix"
|
||||
>
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/statistic/demo/countdown.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-row"
|
||||
style="margin-left:-8px;margin-right:-8px"
|
||||
>
|
||||
<div
|
||||
class="ant-col-12"
|
||||
style="padding-left:8px;padding-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Countdown
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
48:00:30
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col-12"
|
||||
style="padding-left:8px;padding-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Million Seconds
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
48:00:30:000
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col-24"
|
||||
style="padding-left:8px;padding-right:8px;margin-top:32px"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Day Level
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
2 天 0 时 0 分 30 秒
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/statistic/demo/unit.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-row"
|
||||
style="margin-left:-8px;margin-right:-8px"
|
||||
>
|
||||
<div
|
||||
class="ant-col-12"
|
||||
style="padding-left:8px;padding-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Feedback
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-prefix"
|
||||
>
|
||||
<i
|
||||
class="anticon anticon-like"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="like"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M885.9 533.7c16.8-22.2 26.1-49.4 26.1-77.7 0-44.9-25.1-87.4-65.5-111.1a67.67 67.67 0 0 0-34.3-9.3H572.4l6-122.9c1.4-29.7-9.1-57.9-29.5-79.4A106.62 106.62 0 0 0 471 99.9c-52 0-98 35-111.8 85.1l-85.9 311H144c-17.7 0-32 14.3-32 32v364c0 17.7 14.3 32 32 32h601.3c9.2 0 18.2-1.8 26.5-5.4 47.6-20.3 78.3-66.8 78.3-118.4 0-12.6-1.8-25-5.4-37 16.8-22.2 26.1-49.4 26.1-77.7 0-12.6-1.8-25-5.4-37 16.8-22.2 26.1-49.4 26.1-77.7-.2-12.6-2-25.1-5.6-37.1zM184 852V568h81v284h-81zm636.4-353l-21.9 19 13.9 25.4a56.2 56.2 0 0 1 6.9 27.3c0 16.5-7.2 32.2-19.6 43l-21.9 19 13.9 25.4a56.2 56.2 0 0 1 6.9 27.3c0 16.5-7.2 32.2-19.6 43l-21.9 19 13.9 25.4a56.2 56.2 0 0 1 6.9 27.3c0 22.4-13.2 42.6-33.6 51.8H329V564.8l99.5-360.5a44.1 44.1 0 0 1 42.2-32.3c7.6 0 15.1 2.2 21.1 6.7 9.9 7.4 15.2 18.6 14.6 30.5l-9.6 198.4h314.4C829 418.5 840 436.9 840 456c0 16.5-7.2 32.1-19.6 43z"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value-int"
|
||||
>
|
||||
1,128
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col-12"
|
||||
style="padding-left:8px;padding-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic"
|
||||
>
|
||||
<div
|
||||
class="ant-statistic-title"
|
||||
>
|
||||
Unmerged
|
||||
</div>
|
||||
<div
|
||||
class="ant-statistic-content"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value"
|
||||
>
|
||||
<span
|
||||
class="ant-statistic-content-value-int"
|
||||
>
|
||||
93
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="ant-statistic-content-suffix"
|
||||
>
|
||||
/ 100
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
3
components/statistic/__tests__/demo.test.js
Normal file
3
components/statistic/__tests__/demo.test.js
Normal file
@ -0,0 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('statistic');
|
65
components/statistic/__tests__/index.test.js
Normal file
65
components/statistic/__tests__/index.test.js
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import MockDate from 'mockdate';
|
||||
import moment from 'moment';
|
||||
import { mount } from 'enzyme';
|
||||
import Statistic from '..';
|
||||
|
||||
const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));
|
||||
|
||||
describe('Statistic', () => {
|
||||
beforeAll(() => {
|
||||
MockDate.set(moment('2018-11-28 00:00:00'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
MockDate.reset();
|
||||
});
|
||||
|
||||
it('customize formatter', () => {
|
||||
const formatter = jest.fn(() => 93);
|
||||
const wrapper = mount(<Statistic value={1128} formatter={formatter} />);
|
||||
expect(formatter).toBeCalledWith(1128);
|
||||
expect(wrapper.find('.ant-statistic-content-value').text()).toEqual('93');
|
||||
});
|
||||
|
||||
it('not a number', () => {
|
||||
const wrapper = mount(<Statistic value="bamboo" />);
|
||||
expect(wrapper.find('.ant-statistic-content-value').text()).toEqual('bamboo');
|
||||
});
|
||||
|
||||
describe('Countdown', () => {
|
||||
it('render correctly', () => {
|
||||
const now = moment()
|
||||
.add(2, 'd')
|
||||
.add(11, 'h')
|
||||
.add(28, 'm')
|
||||
.add(9, 's')
|
||||
.add(3, 'ms');
|
||||
|
||||
[
|
||||
['H:m:s', '59:28:9'],
|
||||
['HH:mm:ss', '59:28:09'],
|
||||
['HH:mm:ss:SSS', '59:28:09:003'],
|
||||
['DD-HH:mm:ss', '02-11:28:09'],
|
||||
].forEach(([format, value]) => {
|
||||
const wrapper = mount(<Statistic.Countdown format={format} value={now} />);
|
||||
expect(wrapper.find('.ant-statistic-content-value').text()).toEqual(value);
|
||||
});
|
||||
});
|
||||
|
||||
it('time going', async () => {
|
||||
const now = Date.now() + 1000;
|
||||
const wrapper = mount(<Statistic.Countdown value={now} />);
|
||||
wrapper.update();
|
||||
|
||||
// setInterval should work
|
||||
const instance = wrapper.instance();
|
||||
expect(instance.countdownId).not.toBe(undefined);
|
||||
|
||||
await delay(50);
|
||||
|
||||
wrapper.unmount();
|
||||
expect(instance.countdownId).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
31
components/statistic/demo/basic.md
Normal file
31
components/statistic/demo/basic.md
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
order: 0
|
||||
title:
|
||||
zh-CN: 基本
|
||||
en-US: Basic
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
简单的展示。
|
||||
|
||||
## en-US
|
||||
|
||||
Simplest Usage.
|
||||
|
||||
```jsx
|
||||
import { Statistic, Row, Col, Button } from 'antd';
|
||||
|
||||
ReactDOM.render(
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Statistic title="Active Users" value={112893} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="Account Balance (CNY)" value={112893} precision={2} />
|
||||
<Button style={{ marginTop: 16 }} type="primary">Recharge</Button>
|
||||
</Col>
|
||||
</Row>,
|
||||
mountNode
|
||||
);
|
||||
```
|
50
components/statistic/demo/card.md
Normal file
50
components/statistic/demo/card.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
order: 2
|
||||
title:
|
||||
zh-CN: 在卡片中使用
|
||||
en-US: In Card
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
在卡片中展示统计数值。
|
||||
|
||||
## en-US
|
||||
|
||||
Display statistic data in Card.
|
||||
|
||||
```jsx
|
||||
import { Statistic, Card, Row, Col, Icon } from 'antd';
|
||||
|
||||
ReactDOM.render(
|
||||
<div style={{ background: '#ECECEC', padding: '30px' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Active"
|
||||
value={11.28}
|
||||
precision={2}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
prefix={<Icon type="arrow-up" />}
|
||||
suffix="%"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Idle"
|
||||
value={9.3}
|
||||
precision={2}
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
prefix={<Icon type="arrow-down" />}
|
||||
suffix="%"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>,
|
||||
mountNode
|
||||
);
|
||||
```
|
36
components/statistic/demo/countdown.md
Normal file
36
components/statistic/demo/countdown.md
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
order: 3
|
||||
title:
|
||||
zh-CN: 倒计时
|
||||
en-US: Countdown
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
倒计时组件。
|
||||
|
||||
## en-US
|
||||
|
||||
Countdown component.
|
||||
|
||||
```jsx
|
||||
import { Statistic, Row, Col } from 'antd';
|
||||
|
||||
const Countdown = Statistic.Countdown;
|
||||
const deadline = Date.now() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30; // Moment is also OK
|
||||
|
||||
ReactDOM.render(
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Countdown title="Countdown" value={deadline} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Countdown title="Million Seconds" value={deadline} format="HH:mm:ss:SSS" />
|
||||
</Col>
|
||||
<Col span={24} style={{ marginTop: 32 }}>
|
||||
<Countdown title="Day Level" value={deadline} format="D 天 H 时 m 分 s 秒" />
|
||||
</Col>
|
||||
</Row>,
|
||||
mountNode
|
||||
);
|
||||
```
|
30
components/statistic/demo/unit.md
Normal file
30
components/statistic/demo/unit.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
order: 1
|
||||
title:
|
||||
zh-CN: 单位
|
||||
en-US: Unit
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过前缀和后缀添加单位。
|
||||
|
||||
## en-US
|
||||
|
||||
Add unit through `prefix` and `suffix`.
|
||||
|
||||
```jsx
|
||||
import { Statistic, Row, Col, Icon } from 'antd';
|
||||
|
||||
ReactDOM.render(
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Statistic title="Feedback" value={1128} prefix={<Icon type="like" />} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="Unmerged" value={93} suffix="/ 100" />
|
||||
</Col>
|
||||
</Row>,
|
||||
mountNode
|
||||
);
|
||||
```
|
38
components/statistic/index.en-US.md
Normal file
38
components/statistic/index.en-US.md
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
category: Components
|
||||
type: Data Display
|
||||
title: Statistic
|
||||
---
|
||||
|
||||
Display statistic number.
|
||||
|
||||
## When To Use
|
||||
|
||||
* When want to highlight some data.
|
||||
* When want to display statistic data with description.
|
||||
|
||||
## API
|
||||
|
||||
#### Statistic
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ----------- | ---- | ------- |
|
||||
| decimalSeparator | decimal separator | string | - |
|
||||
| formatter | customize value display logic | (value) => ReactNode | - |
|
||||
| precision | precision of input value | number | - |
|
||||
| prefix | prefix node of value | string \| ReactNode | - |
|
||||
| suffix | suffix node of value | string \| ReactNode | - |
|
||||
| title | Display title | string \| ReactNode | - |
|
||||
| value | Display value | string \| number | - |
|
||||
| valueStyle | Set value css style | style | - |
|
||||
|
||||
#### Statistic.Countdown
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ----------- | ---- | ------- |
|
||||
| format | Format as [moment](http://momentjs.com/) | string | 'HH:mm:ss' |
|
||||
| prefix | prefix node of value | string \| ReactNode | - |
|
||||
| suffix | suffix node of value | string \| ReactNode | - |
|
||||
| title | Display title | string \| ReactNode | - |
|
||||
| value | Set target countdown time | number \| moment | - |
|
||||
| valueStyle | Set value css style | style | - |
|
6
components/statistic/index.tsx
Normal file
6
components/statistic/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import Statistic from './Statistic';
|
||||
import Countdown from './Countdown';
|
||||
|
||||
Statistic.Countdown = Countdown;
|
||||
|
||||
export default Statistic;
|
39
components/statistic/index.zh-CN.md
Normal file
39
components/statistic/index.zh-CN.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
category: Components
|
||||
subtitle: 统计数值
|
||||
type: 数据展示
|
||||
title: Statistic
|
||||
---
|
||||
|
||||
展示统计数值。
|
||||
|
||||
## 何时使用
|
||||
|
||||
* 当需要突出某个或某组数字时。
|
||||
* 当需要展示带描述的统计类数据时使用。
|
||||
|
||||
## API
|
||||
|
||||
#### Statistic
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| -------- | ----------- | ---- | ------- |
|
||||
| decimalSeparator | 设置小数点 | string | - |
|
||||
| formatter | 自定义数值展示 | (value) => ReactNode | - |
|
||||
| precision | 数值精度 | number | - |
|
||||
| prefix | 设置数值的前缀 | string \| ReactNode | - |
|
||||
| suffix | 设置数值的后缀 | string \| ReactNode | - |
|
||||
| title | 数值的标题 | string \| ReactNode | - |
|
||||
| value | 数值内容 | string \| number | - |
|
||||
| valueStyle | 设置数值的样式 | style | - |
|
||||
|
||||
#### Statistic.Countdown
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| -------- | ----------- | ---- | ------- |
|
||||
| format | 格式化倒计时展示,参考 [moment](http://momentjs.com/) | string | 'HH:mm:ss' |
|
||||
| prefix | 设置数值的前缀 | string \| ReactNode | - |
|
||||
| suffix | 设置数值的后缀 | string \| ReactNode | - |
|
||||
| title | 数值的标题 | string \| ReactNode | - |
|
||||
| value | 数值内容 | number \| moment | - |
|
||||
| valueStyle | 设置数值的样式 | style | - |
|
38
components/statistic/style/index.less
Normal file
38
components/statistic/style/index.less
Normal file
@ -0,0 +1,38 @@
|
||||
@import '../../style/themes/default';
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
@statistic-prefix-cls: ~'@{ant-prefix}-statistic';
|
||||
|
||||
.@{statistic-prefix-cls} {
|
||||
.reset-component;
|
||||
|
||||
&-title {
|
||||
font-size: @statistic-title-font-size;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&-content {
|
||||
font-size: @statistic-content-font-size;
|
||||
font-family: @statistic-font-family;
|
||||
|
||||
&-value {
|
||||
&-decimal {
|
||||
font-size: @statistic-unit-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
&-prefix,
|
||||
&-suffix {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&-prefix {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&-suffix {
|
||||
margin-left: 4px;
|
||||
font-size: @statistic-unit-font-size;
|
||||
}
|
||||
}
|
||||
}
|
2
components/statistic/style/index.tsx
Normal file
2
components/statistic/style/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
import '../../style/index.less';
|
||||
import './index.less';
|
60
components/statistic/utils.tsx
Normal file
60
components/statistic/utils.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import * as React from 'react';
|
||||
import * as moment from 'moment';
|
||||
import padStart from 'lodash/padStart';
|
||||
import interopDefault from '../_util/interopDefault';
|
||||
|
||||
export type valueType = number | string;
|
||||
export type countdownValueType = valueType | string;
|
||||
|
||||
export type Formatter =
|
||||
| false
|
||||
| 'number'
|
||||
| 'countdown'
|
||||
| ((value: valueType, config?: FormatConfig) => React.ReactNode);
|
||||
|
||||
export interface FormatConfig {
|
||||
formatter?: Formatter;
|
||||
decimalSeparator?: string;
|
||||
precision?: number;
|
||||
prefixCls?: string;
|
||||
}
|
||||
|
||||
export interface CountdownFormatConfig extends FormatConfig {
|
||||
format?: string;
|
||||
}
|
||||
|
||||
// Countdown
|
||||
const timeUnits: [string, number][] = [
|
||||
['Y', 1000 * 60 * 60 * 24 * 365], // years
|
||||
['M', 1000 * 60 * 60 * 24 * 30], // months
|
||||
['D', 1000 * 60 * 60 * 24], // days
|
||||
['H', 1000 * 60 * 60], // hours
|
||||
['m', 1000 * 60], // minutes
|
||||
['s', 1000], // seconds
|
||||
['S', 1], // million seconds
|
||||
];
|
||||
|
||||
function formatTimeStr(duration: number, format: string) {
|
||||
let leftDuration: number = duration;
|
||||
|
||||
return timeUnits.reduce((current, [name, unit]) => {
|
||||
if (current.indexOf(name) !== -1) {
|
||||
const value = Math.floor(leftDuration / unit);
|
||||
leftDuration -= value * unit;
|
||||
return current.replace(new RegExp(`${name}+`, 'g'), (match: string) => {
|
||||
const len = match.length;
|
||||
return padStart(value.toString(), len, '0');
|
||||
});
|
||||
}
|
||||
return current;
|
||||
}, format);
|
||||
}
|
||||
|
||||
export function formatCountdown(value: countdownValueType, config: CountdownFormatConfig) {
|
||||
const { format = '' } = config;
|
||||
const target = interopDefault(moment)(value).valueOf();
|
||||
const current = interopDefault(moment)().valueOf();
|
||||
const diff = Math.max(target - current, 0);
|
||||
|
||||
return formatTimeStr(diff, format);
|
||||
}
|
@ -562,3 +562,10 @@
|
||||
@list-item-meta-margin-bottom: @padding-md;
|
||||
@list-item-meta-avatar-margin-right: @padding-md;
|
||||
@list-item-meta-title-margin-bottom: @padding-sm;
|
||||
|
||||
// Statistic
|
||||
// ---
|
||||
@statistic-title-font-size: @font-size-base;
|
||||
@statistic-content-font-size: 24px;
|
||||
@statistic-unit-font-size: 16px;
|
||||
@statistic-font-family: Tahoma, 'Helvetica Neue', @font-family;
|
||||
|
@ -35,6 +35,7 @@ Array [
|
||||
"message",
|
||||
"Menu",
|
||||
"Modal",
|
||||
"Statistic",
|
||||
"notification",
|
||||
"Pagination",
|
||||
"Popconfirm",
|
||||
|
4
typings/custom-typings.d.ts
vendored
4
typings/custom-typings.d.ts
vendored
@ -93,6 +93,10 @@ declare module '*.json' {
|
||||
|
||||
declare module 'lodash/debounce';
|
||||
|
||||
declare module 'lodash/padStart';
|
||||
|
||||
declare module 'lodash/padEnd';
|
||||
|
||||
declare module 'lodash/uniqBy';
|
||||
|
||||
declare module "raf";
|
||||
|
Loading…
Reference in New Issue
Block a user