Merge pull request #802 from attacking/feat-1.4.0

Statistic Component
pull/1040/head
三点包子 2019-05-28 10:11:48 +08:00 committed by GitHub
commit 15c645fea1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 678 additions and 0 deletions

View File

@ -95,6 +95,8 @@ import { default as Slider } from './slider';
import { default as Spin } from './spin';
import { default as Statistic } from './statistic';
import { default as Steps } from './steps';
import { default as Switch } from './switch';
@ -172,6 +174,7 @@ const components = [
Select,
Slider,
Spin,
Statistic,
Steps,
Switch,
Table,
@ -254,6 +257,7 @@ export {
Select,
Slider,
Spin,
Statistic,
Steps,
Switch,
Table,

View File

@ -0,0 +1,105 @@
import * as moment from 'moment';
import interopDefault from '../_util/interopDefault';
import { cloneElement } from '../_util/vnode';
import { initDefaultProps } from '../_util/props-util';
import Statistic, { StatisticProps } from './Statistic';
import { formatCountdown } from './utils';
const REFRESH_INTERVAL = 1000 / 30;
function getTime(value) {
return interopDefault(moment)(value).valueOf();
}
export default {
name: 'AStatisticCountdown',
props: initDefaultProps(StatisticProps, {
format: 'HH:mm:ss',
}),
data() {
return {
uniKey: 0,
};
},
countdownId: undefined,
mounted() {
this.$nextTick(() => {
this.syncTimer();
});
},
updated() {
this.$nextTick(() => {
this.syncTimer();
});
},
beforeDestroy() {
this.stopTimer();
},
methods: {
syncTimer() {
const { value } = this.$props;
const timestamp = getTime(value);
if (timestamp >= Date.now()) {
this.startTimer();
} else {
this.stopTimer();
}
},
startTimer() {
if (this.countdownId) {
return;
}
this.countdownId = window.setInterval(() => {
this.uniKey++;
}, REFRESH_INTERVAL);
},
stopTimer() {
const { value } = this.$props;
if (this.countdownId) {
clearInterval(this.countdownId);
this.countdownId = undefined;
const timestamp = getTime(value);
if (timestamp < Date.now()) {
this.$emit('finish');
}
}
},
formatCountdown(value, config) {
const { format } = this.$props;
return formatCountdown(value, { ...config, format });
},
// Countdown do not need display the timestamp
valueRenderHtml: node =>
cloneElement(node, {
props: {
title: undefined,
},
}),
},
render() {
return (
<Statistic
key={this.uniKey}
{...{
props: {
...this.$props,
valueRender: this.valueRenderHtml,
formatter: this.formatCountdown,
},
}}
/>
);
},
};

View File

@ -0,0 +1,58 @@
import padEnd from 'lodash/padEnd';
export default {
name: 'AStatisticNumber',
functional: true,
render(h, context) {
const {
value,
formatter,
precision,
decimalSeparator,
groupSeparator = '',
prefixCls,
} = context.props;
let valueNode;
if (typeof formatter === 'function') {
// Customize formatter
valueNode = formatter(value);
} else {
// Internal formatter
const val = String(value);
const cells = val.match(/^(-?)(\d*)(\.(\d+))?$/);
// Process if illegal number
if (!cells) {
valueNode = val;
} else {
const negative = cells[1];
let int = cells[2] || '0';
let decimal = cells[4] || '';
int = int.replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator);
if (typeof precision === 'number') {
decimal = padEnd(decimal, precision, '0').slice(0, precision);
}
if (decimal) {
decimal = `${decimalSeparator}${decimal}`;
}
valueNode = [
<span key="int" class={`${prefixCls}-content-value-int`}>
{negative}
{int}
</span>,
decimal && (
<span key="decimal" class={`${prefixCls}-content-value-decimal`}>
{decimal}
</span>
),
];
}
}
return <span class={`${prefixCls}-content-value`}>{valueNode}</span>;
},
};

View File

@ -0,0 +1,64 @@
import PropTypes from '../_util/vue-types';
import { getComponentFromProp, initDefaultProps } from '../_util/props-util';
import { ConfigConsumerProps } from '../config-provider';
import StatisticNumber from './Number';
export const StatisticProps = {
prefixCls: PropTypes.string,
decimalSeparator: PropTypes.string,
groupSeparator: PropTypes.string,
format: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
valueStyle: PropTypes.any,
valueRender: PropTypes.any,
formatter: PropTypes.any,
precision: PropTypes.number,
prefix: PropTypes.any,
suffix: PropTypes.any,
title: PropTypes.any,
};
export default {
name: 'AStatistic',
props: initDefaultProps(StatisticProps, {
prefixCls: 'ant-statistic',
decimalSeparator: '.',
groupSeparator: ',',
}),
inject: {
configProvider: { default: () => ({}) },
},
render() {
const {
prefixCls: customizePrefixCls,
value = 0,
valueStyle,
valueRender,
} = this.$props;
const getPrefixCls = this.configProvider.getPrefixCls || ConfigConsumerProps.getPrefixCls;
const prefixCls = getPrefixCls('statistic', customizePrefixCls);
const title = getComponentFromProp(this, 'title');
let prefix = getComponentFromProp(this, 'prefix');
let suffix = getComponentFromProp(this, 'suffix');
const formatter = getComponentFromProp(this, 'formatter');
let valueNode = (
<StatisticNumber {...{ props: this.$props }} value={value} formatter={formatter} />
);
if (valueRender) {
valueNode = valueRender(valueNode);
}
return (
<div class={prefixCls}>
{title && <div class={`${prefixCls}-title`}>{title}</div>}
<div style={valueStyle} class={`${prefixCls}-content`}>
{prefix && <span class={`${prefixCls}-content-prefix`}>{prefix}</span>}
{valueNode}
{suffix && <span class={`${prefixCls}-content-suffix`}>{suffix}</span>}
</div>
</div>
);
},
};

View File

@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/statistic/demo/basic.md correctly 1`] = `
<div>
<div class="ant-statistic" style="margin-right: 50px;">
<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 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>
</div>
`;
exports[`renders ./components/statistic/demo/card.md correctly 1`] = `
<div>
<div class="ant-card ant-card-bordered" style="padding: 30px;">
<div class="ant-card-body">
<div class="ant-statistic" style="margin-right: 50px;">
<div class="ant-statistic-title">Feedback</div>
<div class="ant-statistic-content"><span class="ant-statistic-content-prefix"><i slot="prefix" aria-label="icon: like" class="anticon anticon-like"><svg viewBox="64 64 896 896" data-icon="like" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><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"></path></svg></i></span><span class="ant-statistic-content-value"><span class="ant-statistic-content-value-int">1,128</span></span></div>
</div>
<div class="ant-statistic" valueclass="demo-class">
<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">1,234,567,890</span></span><span class="ant-statistic-content-suffix"><span> / 100</span></span></div>
</div>
</div>
</div>
</div>
`;
exports[`renders ./components/statistic/demo/countdown.md correctly 1`] = `
<div>
<div class="ant-statistic" style="margin-right: 50px;">
<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 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>
`;
exports[`renders ./components/statistic/demo/unit.md correctly 1`] = `
<div>
<div class="ant-statistic" style="margin-right: 50px;">
<div class="ant-statistic-title">Feedback</div>
<div class="ant-statistic-content" style="color: rgb(63, 134, 0);"><span class="ant-statistic-content-prefix"><i slot="prefix" aria-label="icon: arrow-up" class="anticon anticon-arrow-up"><svg viewBox="64 64 896 896" data-icon="arrow-up" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><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"></path></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 class="ant-statistic" valueclass="demo-class">
<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">78</span></span><span class="ant-statistic-content-suffix"><span> / 100</span></span></div>
</div>
</div>
`;

View File

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

View File

@ -0,0 +1,18 @@
<cn>
#### 基本
简单展示
</cn>
<us>
#### Basic
Simplest Usage.
</us>
```html
<template>
<div>
<a-statistic title="Active Users" :value="112893" style="margin-right: 50px" />
<a-statistic title="Account Balance (CNY)" :precision="2" :value="112893" />
</div>
</template>
```

View File

@ -0,0 +1,32 @@
<cn>
#### 在卡片中使用
在卡片中展示统计数值。
</cn>
<us>
#### In Card
Display statistic data in Card.
</us>
```html
<template>
<div>
<a-card style="padding: 30px">
<a-statistic
title="Feedback"
:value="1128"
style="margin-right: 50px"
>
<a-icon slot="prefix" type="like" />
</a-statistic>
<a-statistic
title="Unmerged"
:value="1234567890"
valueClass="demo-class"
>
<span slot="suffix"> / 100</span>
</a-statistic>
</a-card>
</div>
</template>
```

View File

@ -0,0 +1,41 @@
<cn>
#### 倒计时
倒计时组件。
</cn>
<us>
#### Countdown
Countdown component.
</us>
```html
<template>
<div>
<a-statistic-countdown
title="Countdown"
:value="deadline"
@finish="onFinish"
style="margin-right: 50px"
/>
<a-statistic-countdown
title="Million Seconds"
:value="deadline"
format="HH:mm:ss:SSS"
/>
</div>
</template>
<script>
export default {
data () {
return {
deadline: Date.now() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30,
}
},
methods: {
onFinish() {
console.log('over');
},
},
}
</script>
```

View File

@ -0,0 +1,50 @@
<script>
import Basic from './basic';
import Card from './card';
import Unit from './unit';
import Countdown from './countdown';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
const md = {
cn: `# Statistic 统计数值
展示统计数值
## 何时使用
- 当需要突出某个或某组数字时
- 当需要展示带描述的统计类数据时使用
## 代码演示`,
us: `# Statistic
Statistics can be used to represent people or objects. It supports images, 'Icon's, or letters.
## Examples
`,
};
export default {
category: 'Components',
subtitle: '统计数值',
type: 'Data Display',
zhType: '数据展示',
title: 'Statistic',
render () {
return (
<div>
<md cn={md.cn} us={md.us}/>
<Basic/>
<br/>
<Unit/>
<br/>
<Card/>
<br/>
<Countdown/>
<br/>
<api>
<template slot='cn'>
<CN/>
</template>
<US/>
</api>
</div>
);
},
};
</script>

View File

@ -0,0 +1,33 @@
<cn>
#### 单位
通过前缀和后缀添加单位。
</cn>
<us>
#### Unit
Add unit through `prefix` and `suffix`.
</us>
```html
<template>
<div>
<a-statistic
title="Feedback"
:value="11.28"
:precision="2"
suffix="%"
:valueStyle="{color: '#3f8600'}"
style="margin-right: 50px"
>
<a-icon slot="prefix" type="arrow-up" />
</a-statistic>
<a-statistic
title="Unmerged"
:value="78"
valueClass="demo-class"
>
<span slot="suffix"> / 100</span>
</a-statistic>
</div>
</template>
```

View File

@ -0,0 +1,30 @@
## API
### Statistic
| Property | Description | Type | Default |
| -------- | ----------- | ---- | ------- |
| decimalSeparator | decimal separator | string | . |
| formatter | customize value display logic | (h) => VNode | - |
| groupSeparator | group separator | string | , |
| precision | precision of input value | number | - |
| prefix | prefix node of value | string \| VNode | - |
| suffix | suffix node of value | string \| VNode | - |
| title | Display title | string \| VNode | - |
| value | Display value | string \| number | - |
### Statistic.Countdown
| Property | Description | Type | Default |
| -------- | ----------- | ---- | ------- |
| format | Format as [moment](http://momentjs.com/) | string | 'HH:mm:ss' |
| prefix | prefix node of value | string \| VNode | - |
| suffix | suffix node of value | string \| VNode | - |
| title | Display title | string \| VNode | - |
| value | Set target countdown time | number \| moment | - |
#### Statistic.Countdown Events
| Events Name | Description | Arguments |
| --- | --- | --- |
| finish | Trigger when time's up | () => void | - |

View File

@ -0,0 +1,11 @@
import Statistic from './Statistic';
import Countdown from './Countdown';
Statistic.Countdown = Countdown;
/* istanbul ignore next */
Statistic.install = function(Vue) {
Vue.component(Statistic.name, Statistic);
Vue.component(Statistic.Countdown.name, Statistic.Countdown);
};
export default Statistic;

View File

@ -0,0 +1,29 @@
## API
### Statistic
| 参数 | 说明 | 类型 | 默认值 |
| -------- | ----------- | ---- | ------- |
| decimalSeparator | 设置小数点 | string | . |
| formatter | 自定义数值展示 | (h) => VNode | - |
| groupSeparator | 设置千分位标识符 | string | , |
| precision | 数值精度 | number | - |
| prefix | 设置数值的前缀 | string \| VNode | - |
| suffix | 设置数值的后缀 | string \| VNode | - |
| title | 数值的标题 | string \| VNode | - |
| value | 数值内容 | string \| number | - |
### Statistic.Countdown
| 参数 | 说明 | 类型 | 默认值 |
| -------- | ----------- | ---- | ------- |
| format | 格式化倒计时展示,参考 [moment](http://momentjs.com/) | string | 'HH:mm:ss' |
| prefix | 设置数值的前缀 | string \| VNode | - |
| suffix | 设置数值的后缀 | string \| VNode | - |
| title | 数值的标题 | string \| VNode | - |
| value | 数值内容 | number \| moment | - |
#### Statistic.Countdown事件
| 事件名称 | 说明 | 回调参数 |
| --- | --- | --- |
| finish | 倒计时完成时触发 | () => void |

View File

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

View File

@ -0,0 +1,23 @@
@import '../../style/themes/default';
@import '../../style/mixins/index';
@statistic-prefix-cls: ~'@{ant-prefix}-statistic';
.@{statistic-prefix-cls} {
.reset-component;
display: inline-block;
white-space: nowrap;
overflow: hidden;
vertical-align: middle;
&-content-value,
&-content-prefix {
font-size: 1.7em;
}
&-content-value-decimal {
font-size: 0.7em;
}
&-content-suffix {
font-size: 1.1em;
}
}

View File

@ -0,0 +1,39 @@
import * as moment from 'moment';
import padStart from 'lodash/padStart';
import interopDefault from '../_util/interopDefault';
// Countdown
const timeUnits = [
['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, format) {
let leftDuration = 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 => {
const len = match.length;
return padStart(value.toString(), len, '0');
});
}
return current;
}, format);
}
export function formatCountdown(value, config) {
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

@ -54,3 +54,4 @@ import './skeleton/style';
import './comment/style';
import './config-provider/style';
import './empty/style';
import './statistic/style';

View File

@ -40,6 +40,7 @@ import {
Select,
Slider,
Spin,
Statistic,
Steps,
Switch,
Table,
@ -110,6 +111,7 @@ Vue.use(Row);
Vue.use(Select);
Vue.use(Slider);
Vue.use(Spin);
Vue.use(Statistic);
Vue.use(Steps);
Vue.use(Switch);
Vue.use(Table);

View File

@ -352,4 +352,10 @@ export default {
title: 'Skeleton',
subtitle: '骨架屏',
},
statistic: {
category: 'Components',
subtitle: '统计数值',
type: 'Data Display',
title: 'Statistic',
},
};

View File

@ -7,6 +7,14 @@ export default [
path: 'avatar-cn',
component: () => import('../components/avatar/demo/index.vue'),
},
{
path: 'statistic',
component: () => import('../components/statistic/demo/index.vue'),
},
{
path: 'statistic-cn',
component: () => import('../components/statistic/demo/index.vue'),
},
{
path: 'badge',
component: () => import('../components/badge/demo/index.vue'),

View File

@ -44,6 +44,7 @@ Array [
"Select",
"Slider",
"Spin",
"Statistic",
"Steps",
"Switch",
"Table",

View File

@ -46,6 +46,7 @@ import { Select } from './select/select';
import { Skeleton } from './skeleton';
import { Slider } from './slider';
import { Spin } from './spin';
import { Statistic } from './statistic';
import { Steps } from './steps/steps';
import { Switch } from './switch';
import { Table } from './table/table';
@ -110,6 +111,7 @@ export {
Select,
Slider,
Spin,
Statistic,
Steps,
Switch,
Table,

59
types/statistic.ts Normal file
View File

@ -0,0 +1,59 @@
// Project: https://github.com/vueComponent/ant-design-vue
// Definitions by: akki-jat <https://github.com/akki-jat>
// Definitions: https://github.com/vueComponent/ant-design-vue/types
import { AntdComponent } from './component';
import { VNode } from 'vue';
export declare class Statistic extends AntdComponent {
/**
* decimal separator
* @default '.'
* @type string
*/
decimalSeparator: string;
/**
* the shape of statistic
* @type string
*/
formatter: () => VNode;
/**
* group separator
* @default ','
* @type string
*/
groupSeparator: string;
/**
* precision of input value
* @type number
*/
precision: number;
/**
* prefix node of value
* @type string | VNode
*/
prefix: string | VNode;
/**
* suffix node of value
* @type string | VNode
*/
suffix: string | VNode;;
/**
* Display title
* @type string | VNode
*/
title: string | VNode;
/**
* Display value
* @type string or number
*/
value: string | number;
}