diff --git a/components/components.less b/components/components.less
index 36c161eabb..fb3a51e9cf 100644
--- a/components/components.less
+++ b/components/components.less
@@ -38,6 +38,7 @@
@import "./skeleton/style/index.less";
@import "./slider/style/index.less";
@import "./spin/style/index.less";
+@import "./statistic/style/index.less";
@import "./steps/style/index.less";
@import "./switch/style/index.less";
@import "./table/style/index.less";
diff --git a/components/core/util/public-api.ts b/components/core/util/public-api.ts
index 969fdecdf9..24ce3fe828 100644
--- a/components/core/util/public-api.ts
+++ b/components/core/util/public-api.ts
@@ -2,5 +2,7 @@ export * from './check';
export * from './convert';
export * from './getMentions';
export * from './nz-global-monitor';
+export * from './string';
export * from './textarea-caret-position';
export * from './throttleByAnimationFrame';
+export * from './time';
diff --git a/components/core/util/string.ts b/components/core/util/string.ts
new file mode 100644
index 0000000000..c3ed73dc06
--- /dev/null
+++ b/components/core/util/string.ts
@@ -0,0 +1,20 @@
+/**
+ * Much like lodash.
+ */
+export function padStart(toPad: string, length: number, element: string): string {
+ if (toPad.length > length) {
+ return toPad;
+ }
+
+ const joined = `${getRepeatedElement(length, element)}${toPad}`;
+ return joined.slice(joined.length - length, joined.length);
+}
+
+export function padEnd(toPad: string, length: number, element: string): string {
+ const joined = `${toPad}${getRepeatedElement(length, element)}`;
+ return joined.slice(0, length);
+}
+
+export function getRepeatedElement(length: number, element: string): string {
+ return Array(length).fill(element).join('');
+}
diff --git a/components/core/util/time.ts b/components/core/util/time.ts
new file mode 100644
index 0000000000..e14f7b76e4
--- /dev/null
+++ b/components/core/util/time.ts
@@ -0,0 +1,9 @@
+export const timeUnits: Array<[ 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
+];
diff --git a/components/icon/nz-icon.service.ts b/components/icon/nz-icon.service.ts
index c835e90d69..c8b9e481e6 100644
--- a/components/icon/nz-icon.service.ts
+++ b/components/icon/nz-icon.service.ts
@@ -45,11 +45,8 @@ export interface NzIconfontOption {
}
export const NZ_ICONS = new InjectionToken('nz_icons');
-
export const NZ_ICON_DEFAULT_TWOTONE_COLOR = new InjectionToken('nz_icon_default_twotone_color');
-
export const DEFAULT_TWOTONE_COLOR = '#1890ff';
-
export const NZ_ICONS_USED_BY_ZORRO: IconDefinition[] = [
BarsOutline,
CalendarOutline,
diff --git a/components/ng-zorro-antd.module.ts b/components/ng-zorro-antd.module.ts
index c5675b6ccb..4e8b5fca60 100644
--- a/components/ng-zorro-antd.module.ts
+++ b/components/ng-zorro-antd.module.ts
@@ -45,6 +45,7 @@ import { NzSelectModule } from './select/nz-select.module';
import { NzSkeletonModule } from './skeleton/nz-skeleton.module';
import { NzSliderModule } from './slider/nz-slider.module';
import { NzSpinModule } from './spin/nz-spin.module';
+import { NzStatisticModule } from './statistic/nz-statistic.module';
import { NzStepsModule } from './steps/nz-steps.module';
import { NzSwitchModule } from './switch/nz-switch.module';
import { NzTableModule } from './table/nz-table.module';
@@ -94,6 +95,7 @@ export * from './radio';
export * from './rate';
export * from './select';
export * from './spin';
+export * from './statistic';
export * from './steps';
export * from './switch';
export * from './table';
@@ -178,6 +180,7 @@ export * from './core/util';
NzTimePickerModule,
NzWaveModule,
NzSkeletonModule,
+ NzStatisticModule,
NzEmptyModule
]
})
diff --git a/components/spin/demo/basic.md b/components/spin/demo/basic.md
index a7479fd8b8..dde5e7be66 100644
--- a/components/spin/demo/basic.md
+++ b/components/spin/demo/basic.md
@@ -2,7 +2,7 @@
order: 0
title:
zh-CN: 基本用法
- en-US: basic Usage
+ en-US: Basic Usage
---
## zh-CN
diff --git a/components/statistic/demo/basic.md b/components/statistic/demo/basic.md
new file mode 100644
index 0000000000..140c34f71b
--- /dev/null
+++ b/components/statistic/demo/basic.md
@@ -0,0 +1,14 @@
+---
+order: 0
+title:
+ zh-CN: 基本用法
+ en-US: Basic Usage
+---
+
+## zh-CN
+
+简单的展示。
+
+## en-US
+
+Simplest Usage.
diff --git a/components/statistic/demo/basic.ts b/components/statistic/demo/basic.ts
new file mode 100644
index 0000000000..90648866be
--- /dev/null
+++ b/components/statistic/demo/basic.ts
@@ -0,0 +1,17 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-statistic-basic',
+ template: `
+
+
+
+
+
+
+
+
+ `
+})
+export class NzDemoStatisticBasicComponent {
+}
diff --git a/components/statistic/demo/card.md b/components/statistic/demo/card.md
new file mode 100644
index 0000000000..e33acc494a
--- /dev/null
+++ b/components/statistic/demo/card.md
@@ -0,0 +1,14 @@
+---
+order: 2
+title:
+ zh-CN: 在卡片中使用
+ en-US: In Card
+---
+
+## zh-CN
+
+在卡片中展示统计数值。
+
+## en-US
+
+Display statistic data in Card.
diff --git a/components/statistic/demo/card.ts b/components/statistic/demo/card.ts
new file mode 100644
index 0000000000..91c5619c1e
--- /dev/null
+++ b/components/statistic/demo/card.ts
@@ -0,0 +1,37 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-statistic-card',
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+})
+export class NzDemoStatisticCardComponent {
+}
diff --git a/components/statistic/demo/countdown.md b/components/statistic/demo/countdown.md
new file mode 100644
index 0000000000..6a82c95a4c
--- /dev/null
+++ b/components/statistic/demo/countdown.md
@@ -0,0 +1,14 @@
+---
+order: 3
+title:
+ zh-CN: 倒计时
+ en-US: Countdown
+---
+
+## zh-CN
+
+倒计时组件。
+
+## en-US
+
+Countdown component.
\ No newline at end of file
diff --git a/components/statistic/demo/countdown.ts b/components/statistic/demo/countdown.ts
new file mode 100644
index 0000000000..cf2d16f4b2
--- /dev/null
+++ b/components/statistic/demo/countdown.ts
@@ -0,0 +1,21 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-statistic-countdown',
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+ `
+})
+export class NzDemoStatisticCountdownComponent {
+ deadline = Date.now() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30;
+}
diff --git a/components/statistic/demo/unit.md b/components/statistic/demo/unit.md
new file mode 100644
index 0000000000..bbba3032dd
--- /dev/null
+++ b/components/statistic/demo/unit.md
@@ -0,0 +1,14 @@
+---
+order: 1
+title:
+ zh-CN: 单位
+ en-US: Unit
+---
+
+## zh-CN
+
+通过前缀和后缀添加单位。
+
+## en-US
+
+Add unit through `nzPrefix` and `nzSuffix`.
diff --git a/components/statistic/demo/unit.ts b/components/statistic/demo/unit.ts
new file mode 100644
index 0000000000..56188c0a32
--- /dev/null
+++ b/components/statistic/demo/unit.ts
@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-statistic-unit',
+ template: `
+
+
+
+
+
+
+
+
+
+ `
+})
+export class NzDemoStatisticUnitComponent {
+}
diff --git a/components/statistic/doc/index.en-US.md b/components/statistic/doc/index.en-US.md
new file mode 100644
index 0000000000..b90e46cee2
--- /dev/null
+++ b/components/statistic/doc/index.en-US.md
@@ -0,0 +1,48 @@
+---
+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
+
+### nz-statistic
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| `[nzPrefix]` | Prefix of Value | `string|TemplateRef` | - |
+| `[nzSuffix]` | Suffix of Value | `string|TemplateRef` | - |
+| `[nzTitle]` | Title | `string|TemplateRef` | - |
+| `[nzValue]` | Value | `string|number` | - |
+| `[nzValueStyle]` | Value CSS style | `Object` | - |
+| `[nzValueTemplate]` | Custom template to render a number | `TemplateRef<{ $implicit: string|number }>` | - |
+
+### nz-countdown
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| `[nzFormat]` | Format string | `string` | `"HH:mm:ss"` |
+| `[nzPrefix]` | Prefix of Value | `string|TemplateRef` | - |
+| `[nzSuffix]` | Suffix of Value | `string|TemplateRef` | - |
+| `[nzTitle]` | Title | `string|TemplateRef` | - |
+| `[nzValue]` | Target time in timestamp form | `string|number` | - |
+| `[nzValueTemplate]` | Custom template to render a time | `TemplateRef<{ $implicit: number }>` | - |
+
+### nzFormat
+
+| Token | Description |
+| -------- | ----------- |
+| `Y` | Year |
+| `M` | Month |
+| `D` | Date |
+| `H` | Hour |
+| `m` | Minute |
+| `s` | Second |
+| `S` | Millisecond |
diff --git a/components/statistic/doc/index.zh-CN.md b/components/statistic/doc/index.zh-CN.md
new file mode 100644
index 0000000000..5a5bc0791f
--- /dev/null
+++ b/components/statistic/doc/index.zh-CN.md
@@ -0,0 +1,49 @@
+---
+category: Components
+title: Statistic
+subtitle: 统计
+type: Data Display
+---
+
+展示统计数字。
+
+## 何时使用
+
+- 当需要突出某个或某组数字时。
+- 当需要展示带描述的统计类数据时使用。
+
+## API
+
+### nz-statistic
+
+| 参数 | 说明 | 类型 | 默认值 |
+| -------- | ----------- | ---- | ------- |
+| `[nzPrefix]` | 设置数值的前缀 | `string|TemplateRef` | - |
+| `[nzSuffix]` | 设置数值的后缀 | `string|TemplateRef` | - |
+| `[nzTitle]` | 数值的标题 | `string|TemplateRef` | - |
+| `[nzValue]` | 数值内容 | `string|number` | - |
+| `[nzValueStyle]` | 设置数值的样式 | `Object` | - |
+| `[nzValueTemplate]` | 自定义数值展示 | `TemplateRef<{ $implicit: string|number }>` | - |
+
+### nz-countdown
+
+| 参数 | 说明 | 类型 | 默认值 |
+| -------- | ----------- | ---- | ------- |
+| `[nzFormat]` | 格式化倒计时展示 | `string` | `"HH:mm:ss"` |
+| `[nzPrefix]` | 设置数值的前缀 | `string|TemplateRef` | - |
+| `[nzSuffix]` | 设置数值的后缀 | `string|TemplateRef` | - |
+| `[nzTitle]` | 数值的标题 | `string|TemplateRef` | - |
+| `[nzValue]` | 时间戳格式的目标时间 | `string|number` | - |
+| `[nzValueTemplate]` | 自定义时间展示 | `TemplateRef<{ $implicit: number }>` | - |
+
+### nzFormat
+
+| 占位符 | 描述 |
+| -------- | ----------- |
+| `Y` | 年 |
+| `M` | 月 |
+| `D` | 日 |
+| `H` | 时 |
+| `m` | 分 |
+| `s` | 秒 |
+| `S` | 毫秒 |
diff --git a/components/statistic/index.ts b/components/statistic/index.ts
new file mode 100644
index 0000000000..7e1a213e3e
--- /dev/null
+++ b/components/statistic/index.ts
@@ -0,0 +1 @@
+export * from './public-api';
diff --git a/components/statistic/nz-countdown.component.html b/components/statistic/nz-countdown.component.html
new file mode 100644
index 0000000000..4b6ba134dd
--- /dev/null
+++ b/components/statistic/nz-countdown.component.html
@@ -0,0 +1,10 @@
+
+
+
+{{ diff | nzTimeRange: nzFormat }}
\ No newline at end of file
diff --git a/components/statistic/nz-countdown.component.ts b/components/statistic/nz-countdown.component.ts
new file mode 100644
index 0000000000..78161ed93e
--- /dev/null
+++ b/components/statistic/nz-countdown.component.ts
@@ -0,0 +1,90 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Input,
+ NgZone,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ SimpleChanges,
+ ViewEncapsulation
+} from '@angular/core';
+import { interval, Subscription } from 'rxjs';
+
+import { REFRESH_INTERVAL } from './nz-statistic-definitions';
+import { NzStatisticComponent } from './nz-statistic.component';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation : ViewEncapsulation.None,
+ selector : 'nz-countdown',
+ templateUrl : './nz-countdown.component.html'
+})
+export class NzCountdownComponent extends NzStatisticComponent implements OnInit, OnChanges, OnDestroy {
+ /** @override */
+ @Input() nzFormat: string = 'HH:mm:ss';
+
+ diff: number;
+
+ private target: number;
+ private updater_: Subscription;
+
+ constructor(private cdr: ChangeDetectorRef, private ngZone: NgZone) {
+ super();
+ }
+
+ /** @override */
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.nzValue) {
+ this.target = Number(changes.nzValue.currentValue);
+ if (!changes.nzValue.isFirstChange()) {
+ this.syncTimer();
+ }
+ }
+ }
+
+ ngOnInit(): void {
+ this.syncTimer();
+ }
+
+ ngOnDestroy(): void {
+ this.stopTimer();
+ }
+
+ syncTimer(): void {
+ if (this.target >= Date.now()) {
+ this.startTimer();
+ } else {
+ this.stopTimer();
+ }
+ }
+
+ startTimer(): void {
+ this.ngZone.runOutsideAngular(() => {
+ this.stopTimer();
+ this.updater_ = interval(REFRESH_INTERVAL).subscribe(() => {
+ this.updateValue();
+ this.cdr.detectChanges();
+ });
+ });
+ }
+
+ stopTimer(): void {
+ if (this.updater_) {
+ this.updater_.unsubscribe();
+ this.updater_ = null;
+ }
+ }
+
+ /**
+ * Update time that should be displayed on the screen.
+ */
+ protected updateValue(): void {
+ this.diff = Math.max(this.target - Date.now(), 0);
+
+ if (this.diff === 0) {
+ this.stopTimer();
+ }
+ }
+}
diff --git a/components/statistic/nz-countdown.spec.ts b/components/statistic/nz-countdown.spec.ts
new file mode 100644
index 0000000000..29c1a55b21
--- /dev/null
+++ b/components/statistic/nz-countdown.spec.ts
@@ -0,0 +1,91 @@
+import { CommonModule } from '@angular/common';
+import { Component, TemplateRef, ViewChild } from '@angular/core';
+import { fakeAsync, tick, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { NzCountdownComponent } from './nz-countdown.component';
+import { NzStatisticModule } from './nz-statistic.module';
+
+describe('nz-countdown', () => {
+ let fixture;
+ let testComponent;
+ let countdownEl;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports : [ CommonModule, NzStatisticModule ],
+ declarations: [ NzTestCountdownComponent ]
+ }).compileComponents();
+ });
+
+ describe('basic', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NzTestCountdownComponent);
+ testComponent = fixture.debugElement.componentInstance;
+ countdownEl = fixture.debugElement.query(By.directive(NzCountdownComponent));
+ });
+
+ it('should render time', fakeAsync(() => {
+ testComponent.resetTimerWithFormat('HH:mm:ss');
+ fixture.detectChanges();
+ tick(100);
+ fixture.detectChanges();
+ expect(countdownEl.nativeElement.querySelector('.ant-statistic-content-value').innerText).toBe('48:00:29');
+ testComponent.countdown.stopTimer();
+ }));
+
+ it('should stop timer when nzValue is earlier than current', fakeAsync(() => {
+ const beforeTime = new Date().getTime() - 1000 * 1000;
+ const spyOnStop = spyOn(testComponent.countdown, 'stopTimer');
+ testComponent.value = beforeTime;
+
+ fixture.detectChanges();
+ tick(100);
+ fixture.detectChanges();
+ expect(countdownEl.nativeElement.querySelector('.ant-statistic-content-value').innerText).toBe('00:00:00');
+ expect(spyOnStop).toHaveBeenCalledTimes(1);
+ }));
+
+ it('should support template', fakeAsync(() => {
+ testComponent.resetWithTemplate();
+ fixture.detectChanges();
+ tick(100);
+ fixture.detectChanges();
+
+ expect(countdownEl.nativeElement.querySelector('.ant-statistic-content-value').innerText).toBe('172829900');
+ testComponent.countdown.stopTimer();
+ }));
+ });
+});
+
+@Component({
+ selector: 'nz-test',
+ template: `
+
+
+
+ {{ diff }}
+
+ `
+})
+export class NzTestCountdownComponent {
+ @ViewChild(NzCountdownComponent) countdown: NzCountdownComponent;
+ @ViewChild('tpl') tpl: TemplateRef;
+
+ format;
+ value;
+ template;
+
+ resetTimerWithFormat(format: string): void {
+ this.format = format;
+ this.value = new Date().getTime() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30;
+ }
+
+ resetWithTemplate(): void {
+ this.template = this.tpl;
+ this.value = new Date().getTime() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30;
+ }
+}
diff --git a/components/statistic/nz-statistic-definitions.ts b/components/statistic/nz-statistic-definitions.ts
new file mode 100644
index 0000000000..a5ad8b4d6f
--- /dev/null
+++ b/components/statistic/nz-statistic-definitions.ts
@@ -0,0 +1,3 @@
+export type NzStatisticValueType = number | string;
+
+export const REFRESH_INTERVAL = 1000 / 30;
diff --git a/components/statistic/nz-statistic-number.component.html b/components/statistic/nz-statistic-number.component.html
new file mode 100644
index 0000000000..4dc3d0267f
--- /dev/null
+++ b/components/statistic/nz-statistic-number.component.html
@@ -0,0 +1,9 @@
+
+
+
+ {{ displayInt }}
+ {{ displayDecimal }}
+
diff --git a/components/statistic/nz-statistic-number.component.ts b/components/statistic/nz-statistic-number.component.ts
new file mode 100644
index 0000000000..5c61940597
--- /dev/null
+++ b/components/statistic/nz-statistic-number.component.ts
@@ -0,0 +1,48 @@
+import { getLocaleNumberSymbol, NumberSymbol } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Inject,
+ Input,
+ LOCALE_ID,
+ OnChanges,
+ TemplateRef,
+ ViewEncapsulation
+} from '@angular/core';
+import { NzStatisticValueType } from './nz-statistic-definitions';
+
+@Component({
+ changeDetection : ChangeDetectionStrategy.OnPush,
+ encapsulation : ViewEncapsulation.None,
+ preserveWhitespaces: false,
+ selector : 'nz-statistic-number',
+ templateUrl : './nz-statistic-number.component.html',
+ host : {
+ 'class': 'ant-statistic-content-value'
+ },
+ styles : [ 'nz-number { display: inline }' ]
+})
+export class NzStatisticNumberComponent implements OnChanges {
+ @Input() nzValue: NzStatisticValueType;
+ @Input() nzValueTemplate: TemplateRef<{ $implicit: NzStatisticValueType }>;
+
+ displayInt = '';
+ displayDecimal = '';
+
+ constructor(@Inject(LOCALE_ID) private locale_id: string) {}
+
+ ngOnChanges(): void {
+ this.formatNumber();
+ }
+
+ private formatNumber(): void {
+ const decimalSeparator: string = typeof this.nzValue === 'number'
+ ? '.'
+ : getLocaleNumberSymbol(this.locale_id, NumberSymbol.Decimal);
+ const value = String(this.nzValue);
+ const [ int, decimal ] = value.split(decimalSeparator);
+
+ this.displayInt = int;
+ this.displayDecimal = decimal ? `${decimalSeparator}${decimal}` : '';
+ }
+}
diff --git a/components/statistic/nz-statistic-number.spec.ts b/components/statistic/nz-statistic-number.spec.ts
new file mode 100644
index 0000000000..75aca64de4
--- /dev/null
+++ b/components/statistic/nz-statistic-number.spec.ts
@@ -0,0 +1,65 @@
+import { CommonModule } from '@angular/common';
+import { Component, TemplateRef, ViewChild } from '@angular/core';
+import { TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { NzStatisticNumberComponent } from './nz-statistic-number.component';
+import { NzStatisticModule } from './nz-statistic.module';
+
+describe('nz-number', () => {
+ let fixture;
+ let testComponent;
+ let numberEl;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports : [ CommonModule, NzStatisticModule ],
+ declarations: [ NzTestNumberComponent ]
+ }).compileComponents();
+ });
+
+ describe('basic', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NzTestNumberComponent);
+ testComponent = fixture.debugElement.componentInstance;
+ numberEl = fixture.debugElement.query(By.directive(NzStatisticNumberComponent));
+ });
+
+ it('should have correct class', () => {
+ fixture.detectChanges();
+ expect(numberEl.nativeElement.classList.contains('ant-statistic-content-value')).toBeTruthy();
+ });
+
+ it('should render number', () => {
+ // Int with group separator, decimal.
+ testComponent.value = 12345.012;
+ fixture.detectChanges();
+ expect(numberEl.nativeElement.querySelector('.ant-statistic-content-value-int').innerText).toBe('12,345');
+ expect(numberEl.nativeElement.querySelector('.ant-statistic-content-value-decimal').innerText).toBe('.012');
+ });
+
+ it('should support template', () => {
+ testComponent.template = testComponent.tpl;
+ testComponent.value = 12345.012;
+ fixture.detectChanges();
+ expect(numberEl.nativeElement.querySelector('.ant-statistic-content-value-int')).toBeFalsy();
+ expect(numberEl.nativeElement.querySelector('.ant-statistic-content-value-decimal')).toBeFalsy();
+ expect(numberEl.nativeElement.innerText).toBe('It\'s 12,345.012');
+ });
+ });
+});
+
+@Component({
+ selector: 'nz-test-number-component',
+ template: `
+
+ It's {{ value }}
+ `
+})
+export class NzTestNumberComponent {
+ @ViewChild('tpl') tpl: TemplateRef;
+
+ value = 1;
+ template;
+}
diff --git a/components/statistic/nz-statistic.component.html b/components/statistic/nz-statistic.component.html
new file mode 100644
index 0000000000..018bb1d141
--- /dev/null
+++ b/components/statistic/nz-statistic.component.html
@@ -0,0 +1,15 @@
+
+ {{ nzTitle }}
+
+
+
+ {{ nzPrefix }}
+
+
+
+
+ {{ nzSuffix }}
+
+
diff --git a/components/statistic/nz-statistic.component.ts b/components/statistic/nz-statistic.component.ts
new file mode 100644
index 0000000000..81a32b46a2
--- /dev/null
+++ b/components/statistic/nz-statistic.component.ts
@@ -0,0 +1,27 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Input,
+ TemplateRef,
+ ViewEncapsulation
+} from '@angular/core';
+import { NzStatisticValueType } from './nz-statistic-definitions';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation : ViewEncapsulation.None,
+ selector : 'nz-statistic',
+ templateUrl : './nz-statistic.component.html',
+ host : {
+ class: 'ant-statistic'
+ },
+ styles : [ 'nz-statistic { display: block; }' ]
+})
+export class NzStatisticComponent {
+ @Input() nzPrefix: string | TemplateRef;
+ @Input() nzSuffix: string | TemplateRef;
+ @Input() nzTitle: string | TemplateRef;
+ @Input() nzValue: NzStatisticValueType;
+ @Input() nzValueStyle = {};
+ @Input() nzValueTemplate: TemplateRef<{ $implicit: NzStatisticValueType }>;
+}
diff --git a/components/statistic/nz-statistic.module.ts b/components/statistic/nz-statistic.module.ts
new file mode 100644
index 0000000000..76b497ef2f
--- /dev/null
+++ b/components/statistic/nz-statistic.module.ts
@@ -0,0 +1,16 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { NzAddOnModule } from '../core/addon/addon.module';
+import { NzCountdownComponent } from './nz-countdown.component';
+import { NzStatisticNumberComponent } from './nz-statistic-number.component';
+import { NzStatisticComponent } from './nz-statistic.component';
+import { NzTimeRangePipe } from './nz-time-range.pipe';
+
+@NgModule({
+ imports : [ CommonModule, NzAddOnModule ],
+ declarations: [ NzStatisticComponent, NzCountdownComponent, NzStatisticNumberComponent, NzTimeRangePipe ],
+ exports : [ NzStatisticComponent, NzCountdownComponent, NzStatisticNumberComponent, NzTimeRangePipe ]
+})
+export class NzStatisticModule {
+}
diff --git a/components/statistic/nz-statistic.spec.ts b/components/statistic/nz-statistic.spec.ts
new file mode 100644
index 0000000000..9a21356855
--- /dev/null
+++ b/components/statistic/nz-statistic.spec.ts
@@ -0,0 +1,57 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { NzStatisticComponent } from './nz-statistic.component';
+import { NzStatisticModule } from './nz-statistic.module';
+
+describe('nz-statistic', () => {
+ let fixture;
+ let testComponent;
+ let statisticEl;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports : [ CommonModule, NzStatisticModule ],
+ declarations: [ NzTestStatisticComponent ]
+ }).compileComponents();
+ });
+
+ describe('basic', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NzTestStatisticComponent);
+ testComponent = fixture.debugElement.componentInstance;
+ statisticEl = fixture.debugElement.query(By.directive(NzStatisticComponent));
+ });
+
+ it('should render title, prefix and suffix', () => {
+ fixture.detectChanges();
+ expect(statisticEl.nativeElement.querySelector('.ant-statistic-title').innerText).toBe('title');
+ expect(statisticEl.nativeElement.querySelector('.ant-statistic-content-prefix')).toBeFalsy();
+ expect(statisticEl.nativeElement.querySelector('.ant-statistic-content-suffix')).toBeFalsy();
+
+ testComponent.prefix = 'prefix';
+ testComponent.suffix = 'suffix';
+ fixture.detectChanges();
+ expect(statisticEl.nativeElement.querySelector('.ant-statistic-content-prefix').innerText).toBe('prefix');
+ expect(statisticEl.nativeElement.querySelector('.ant-statistic-content-suffix').innerText).toBe('suffix');
+ });
+ });
+});
+
+@Component({
+ selector: 'nz-test-statistic',
+ template: `
+
+
+ `
+})
+export class NzTestStatisticComponent {
+ title = 'title';
+ prefix = '';
+ suffix = '';
+}
diff --git a/components/statistic/nz-time-range.pipe.ts b/components/statistic/nz-time-range.pipe.ts
new file mode 100644
index 0000000000..afc749e159
--- /dev/null
+++ b/components/statistic/nz-time-range.pipe.ts
@@ -0,0 +1,26 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { padStart, timeUnits } from '../core/util';
+
+@Pipe({
+ name: 'nzTimeRange',
+ pure: true
+})
+export class NzTimeRangePipe implements PipeTransform {
+ transform(value: string | number, format: string = 'HH:mm:ss'): string {
+ let duration = Number(value || 0);
+
+ return timeUnits.reduce((current, [ name, unit ]) => {
+ if (current.indexOf(name) !== -1) {
+ const v = Math.floor(duration / unit);
+ duration -= v * unit;
+ return current.replace(
+ new RegExp(`${name}+`, 'g'),
+ (match: string) => {
+ return padStart(v.toString(), match.length, '0');
+ }
+ );
+ }
+ return current;
+ }, format);
+ }
+}
diff --git a/components/statistic/nz-time-range.spec.ts b/components/statistic/nz-time-range.spec.ts
new file mode 100644
index 0000000000..bfd453e42e
--- /dev/null
+++ b/components/statistic/nz-time-range.spec.ts
@@ -0,0 +1,57 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NzStatisticModule } from './nz-statistic.module';
+
+describe('nz time range pipeline', () => {
+ let fixture: ComponentFixture;
+ let testComponent: NzTestTimeRangeComponent;
+ let element: HTMLElement;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports : [ CommonModule, NzStatisticModule ],
+ declarations: [ NzTestTimeRangeComponent ]
+ }).compileComponents();
+ });
+
+ describe('basic', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NzTestTimeRangeComponent);
+ testComponent = fixture.debugElement.componentInstance;
+ element = fixture.debugElement.nativeElement;
+ });
+
+ it('should render time correctly with different formats', () => {
+ fixture.detectChanges();
+ expect(element.innerText).toBe('48:00:30');
+
+ testComponent.format = 'HH:mm';
+ fixture.detectChanges();
+ expect(element.innerText).toBe('48:00');
+
+ testComponent.format = 'D 天 H 时 m 分 s 秒';
+ fixture.detectChanges();
+ expect(element.innerText).toBe('2 天 0 时 0 分 30 秒');
+ });
+
+ it('should render time correctly with different values', () => {
+ testComponent.diff = 0;
+ fixture.detectChanges();
+ expect(element.innerText).toBe('00:00:00');
+
+ testComponent.diff = - 1000 * 60 * 60 * 24 * 2 + 1000 * 30;
+ fixture.detectChanges();
+ expect(element.innerText).toBe('-48:00:30');
+ });
+ });
+});
+
+@Component({
+ selector: 'nz-test-statistic',
+ template: `{{ diff | nzTimeRange: format }}`
+})
+export class NzTestTimeRangeComponent {
+ diff = 1000 * 60 * 60 * 24 * 2 + 1000 * 30;
+ format = 'HH:mm:ss';
+}
diff --git a/components/statistic/public-api.ts b/components/statistic/public-api.ts
new file mode 100644
index 0000000000..a8402dfb12
--- /dev/null
+++ b/components/statistic/public-api.ts
@@ -0,0 +1,3 @@
+export * from './nz-countdown.component';
+export * from './nz-statistic.component';
+export * from './nz-statistic.module';
diff --git a/components/statistic/style/index.less b/components/statistic/style/index.less
new file mode 100644
index 0000000000..541457a852
--- /dev/null
+++ b/components/statistic/style/index.less
@@ -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;
+ }
+ }
+}