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:
zombieJ 2019-01-09 20:38:09 +08:00 committed by GitHub
parent 54463c908b
commit ff13ac0f5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1065 additions and 3 deletions

View File

@ -35,6 +35,7 @@ Array [
"message",
"Menu",
"Modal",
"Statistic",
"notification",
"Pagination",
"Popconfirm",

View File

@ -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"

View File

@ -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 };

View File

@ -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;
};
}

View File

@ -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';

View 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;

View 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;

View 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;

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

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('statistic');

View 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);
});
});
});

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

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

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

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

View 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 | - |

View File

@ -0,0 +1,6 @@
import Statistic from './Statistic';
import Countdown from './Countdown';
Statistic.Countdown = Countdown;
export default Statistic;

View 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 | - |

View 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;
}
}
}

View File

@ -0,0 +1,2 @@
import '../../style/index.less';
import './index.less';

View 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);
}

View File

@ -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;

View File

@ -35,6 +35,7 @@ Array [
"message",
"Menu",
"Modal",
"Statistic",
"notification",
"Pagination",
"Popconfirm",

View File

@ -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";