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