diff --git a/docs/en-US/component/statistic.md b/docs/en-US/component/statistic.md index 56dc1371d1..910ffe6c9b 100644 --- a/docs/en-US/component/statistic.md +++ b/docs/en-US/component/statistic.md @@ -11,13 +11,13 @@ Display statistics. :::demo To highlight a number or a group of numbers, such as statistical value, amount, and ranking, you can add elements such as icon and unit before and after the number and title. -statistic/num +statistic/basic ::: ## Countdown -:::demo Set `timeIndices`Start the countdown. Countdown component, support English and Chinese countdown, support to add other components control countdown. +:::demo Countdown component, support to add other components control countdown. statistic/countdown ::: @@ -40,10 +40,10 @@ statistic/card ### Statistic Attributes | Attribute | Description | Type | Default | -|-------------------|--------------------------------|---------------------------------------------------------------------|---------| -| value | Numerical content | ^[string] / ^[number] | 0 | +| ----------------- | ------------------------------ | ------------------------------------------------------------------- | ------- | +| value | Numerical content | ^[number] | 0 | | decimal-separator | Setting the decimal point | ^[string] | . | -| formatter | Custom numerical presentation | ^[Function]`(value: string \| number) => string \| number` | — | +| formatter | Custom numerical presentation | ^[Function]`(value: number) => string \| number` | — | | group-separator | Sets the thousandth identifier | ^[string] | , | | precision | numerical precision | ^[number] | 0 | | prefix | Sets the prefix of a number | ^[string] | — | @@ -54,7 +54,7 @@ statistic/card ### Statistic Slots | Name | Description | -|--------|-----------------------------| +| ------ | --------------------------- | | prefix | Numeric prefix | | suffix | Suffixes for numeric values | | title | Numeric titles | @@ -62,15 +62,15 @@ statistic/card ### Statistic Exposes | Name | Description | Type | -|--------------|-----------------------|----------------------------------| -| displayValue | Current display value | ^[object]`Ref` | +| ------------ | --------------------- | -------------------------------- | +| displayValue | current display value | ^[object]`Ref` | ## Countdown API ### Countdown Attributes | Attribute | Description | Type | Default | -|-------------|----------------------------------|---------------------------------------------------------------------|----------| +| ----------- | -------------------------------- | ------------------------------------------------------------------- | -------- | | value | target time | ^[number] / ^[Dayjs] | — | | format | Formatting the countdown display | ^[string] | HH:mm:ss | | prefix | Sets the prefix of a countdown | ^[string] | — | @@ -81,14 +81,14 @@ statistic/card ### Countdown Events | Method | Description | Type | -|--------|------------------------------|--------------------------------------| +| ------ | ---------------------------- | ------------------------------------ | | change | Time difference change event | ^[Function]`(value: number) => void` | | finish | countdown end event | ^[Function]`() => void` | ### Countdown Slots | Name | Description | -|--------|------------------------| +| ------ | ---------------------- | | prefix | countdown value prefix | | suffix | countdown value suffix | | title | countdown title | @@ -96,9 +96,5 @@ statistic/card ### Countdown Exposes | Name | Description | Type | -|--------------|-----------------------|------------------------| -| displayValue | Current display value | ^[object]`Ref` | - - +| ------------ | --------------------- | ---------------------- | +| displayValue | current display value | ^[object]`Ref` | diff --git a/docs/examples/statistic/basic.vue b/docs/examples/statistic/basic.vue new file mode 100644 index 0000000000..1725114acc --- /dev/null +++ b/docs/examples/statistic/basic.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/docs/examples/statistic/card.vue b/docs/examples/statistic/card.vue index 4f3a7fe54c..137dea9b1b 100644 --- a/docs/examples/statistic/card.vue +++ b/docs/examples/statistic/card.vue @@ -1,233 +1,166 @@ - - diff --git a/docs/examples/statistic/countdown.vue b/docs/examples/statistic/countdown.vue index 75fd17ceb1..f9cf0e0f4f 100644 --- a/docs/examples/statistic/countdown.vue +++ b/docs/examples/statistic/countdown.vue @@ -1,75 +1,54 @@ - - diff --git a/docs/examples/statistic/index.scss b/docs/examples/statistic/index.scss deleted file mode 100644 index 80b6fff88b..0000000000 --- a/docs/examples/statistic/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -h2#card-usage ~ .example .example-showcase { - background-color: var(--el-fill-color) !important; -} diff --git a/docs/examples/statistic/num.vue b/docs/examples/statistic/num.vue deleted file mode 100644 index 362ea9279e..0000000000 --- a/docs/examples/statistic/num.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - diff --git a/packages/components/countdown/__tests__/countdown.test.tsx b/packages/components/countdown/__tests__/countdown.test.tsx new file mode 100644 index 0000000000..9173394cda --- /dev/null +++ b/packages/components/countdown/__tests__/countdown.test.tsx @@ -0,0 +1,73 @@ +import { mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import dayjs from 'dayjs' +import Countdown from '../src/countdown.vue' + +import type { VueWrapper } from '@vue/test-utils' + +const TITLE_CLASS = '.el-statistic__head' +const CONTENT_CLASS = '.el-statistic__content' + +describe('Countdown.vue', () => { + let wrapper: VueWrapper + + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + wrapper.unmount() + }) + + it('render test', () => { + wrapper = mount(() => ( + + )) + + expect(wrapper.find(TITLE_CLASS).text()).toBe('test') + expect(wrapper.find(CONTENT_CLASS).text()).toBe('00:01:00') + }) + + describe('format', () => { + it.each([ + ['DD HH:mm:ss', '02 02:02:02'], + ['DD [days] HH [hours] mm:ss', '02 days 02 hours 02:02'], + ['HH:mm:ss', '50:02:02'], + ['HH:mm:ss:SSS', '50:02:02:002'], + ])('should work with %s', (format, expected) => { + const value = dayjs() + .add(2, 'd') + .add(2, 'h') + .add(2, 'm') + .add(2, 's') + .add(2, 'ms') + + wrapper = mount(() => ) + + expect(wrapper.find(CONTENT_CLASS).text()).toBe(expected) + }) + }) + + it('change event', () => { + const onChange = vi.fn() + wrapper = mount(() => ( + + )) + + vi.advanceTimersByTime(16) + expect(onChange).toHaveBeenCalledTimes(1) + }) + + it('finish event', () => { + const onFinish = vi.fn() + wrapper = mount(() => ( + + )) + + vi.advanceTimersByTime(1000 * 30) + expect(onFinish).not.toHaveBeenCalled() + vi.runAllTimers() + expect(onFinish).toHaveBeenCalled() + }) +}) diff --git a/packages/components/countdown/index.ts b/packages/components/countdown/index.ts new file mode 100644 index 0000000000..a722c8247d --- /dev/null +++ b/packages/components/countdown/index.ts @@ -0,0 +1,7 @@ +import { withInstall } from '@element-plus/utils' +import Countdown from './src/countdown.vue' + +export const ElCountdown = withInstall(Countdown) +export default ElCountdown + +export * from './src/countdown' diff --git a/packages/components/countdown/src/countdown.ts b/packages/components/countdown/src/countdown.ts new file mode 100644 index 0000000000..12e5688163 --- /dev/null +++ b/packages/components/countdown/src/countdown.ts @@ -0,0 +1,50 @@ +import { buildProps, definePropType, isNumber } from '@element-plus/utils' +import { CHANGE_EVENT } from '@element-plus/constants' + +import type { ExtractPropTypes, StyleValue } from 'vue' +import type { Dayjs } from 'dayjs' +import type Countdown from './countdown.vue' + +export const countdownProps = buildProps({ + /** + * @description Formatting the countdown display + */ + format: { + type: String, + default: 'HH:mm:ss', + }, + /** + * @description Sets the prefix of a countdown + */ + prefix: String, + /** + * @description Sets the suffix of a countdown + */ + suffix: String, + /** + * @description countdown titles + */ + title: String, + /** + * @description target time + */ + value: { + type: definePropType([Number, Object]), + default: 0, + }, + /** + * @description Styles countdown values + */ + valueStyle: { + type: definePropType([String, Object, Array]), + }, +} as const) +export type CountdownProps = ExtractPropTypes + +export const countdownEmits = { + finish: () => true, + [CHANGE_EVENT]: (value: number) => isNumber(value), +} +export type CountdownEmits = typeof countdownEmits + +export type CountdownInstance = InstanceType diff --git a/packages/components/countdown/src/countdown.vue b/packages/components/countdown/src/countdown.vue new file mode 100644 index 0000000000..d5f0299431 --- /dev/null +++ b/packages/components/countdown/src/countdown.vue @@ -0,0 +1,80 @@ + + diff --git a/packages/components/countdown/src/utils.ts b/packages/components/countdown/src/utils.ts new file mode 100644 index 0000000000..629e9508b2 --- /dev/null +++ b/packages/components/countdown/src/utils.ts @@ -0,0 +1,36 @@ +import { isNumber } from '@element-plus/utils' + +import type { Dayjs } from 'dayjs' + +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 +] as const + +export const getTime = (value: number | Dayjs) => { + return isNumber(value) ? new Date(value).getTime() : value.valueOf() +} + +export const formatTime = (timestamp: number, format: string) => { + let timeLeft = timestamp + const escapeRegex = /\[([^\]]*)]/g + + const replacedText = timeUnits.reduce((current, [name, unit]) => { + const replaceRegex = new RegExp(`${name}+(?![^\\[\\]]*\\])`, 'g') + if (replaceRegex.test(current)) { + const value = Math.floor(timeLeft / unit) + timeLeft -= value * unit + return current.replace(replaceRegex, (match) => + String(value).padStart(match.length, '0') + ) + } + return current + }, format) + + return replacedText.replace(escapeRegex, '$1') +} diff --git a/packages/components/countdown/style/css.ts b/packages/components/countdown/style/css.ts new file mode 100644 index 0000000000..aa3c71325a --- /dev/null +++ b/packages/components/countdown/style/css.ts @@ -0,0 +1,2 @@ +import '@element-plus/components/base/style/css' +import '@element-plus/theme-chalk/el-statistic.css' diff --git a/packages/components/countdown/style/index.ts b/packages/components/countdown/style/index.ts new file mode 100644 index 0000000000..9d21e2a4f2 --- /dev/null +++ b/packages/components/countdown/style/index.ts @@ -0,0 +1,2 @@ +import '@element-plus/components/base/style' +import '@element-plus/theme-chalk/src/statistic.scss' diff --git a/packages/components/index.ts b/packages/components/index.ts index 31f3fad458..a3bf610159 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -19,6 +19,7 @@ export * from './collapse-transition' export * from './color-picker' export * from './config-provider' export * from './container' +export * from './countdown' export * from './date-picker' export * from './descriptions' export * from './dialog' @@ -50,6 +51,7 @@ export * from './select-v2' export * from './skeleton' export * from './slider' export * from './space' +export * from './statistic' export * from './steps' export * from './switch' export * from './table' @@ -66,7 +68,6 @@ export * from './tree-select' export * from './tree-v2' export * from './upload' export * from './virtual-list' -export * from './statistic' // plugins export * from './infinite-scroll' diff --git a/packages/components/statistic/__tests__/countdown.test.tsx b/packages/components/statistic/__tests__/countdown.test.tsx deleted file mode 100644 index ededb0b7a2..0000000000 --- a/packages/components/statistic/__tests__/countdown.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { mount } from '@vue/test-utils' -import { describe, expect, it, vi } from 'vitest' -import dayjs from 'dayjs' -import Countdown from '../src/countdown.vue' - -const TITLE_CLASS = '.el-statistic__head' -const CONTENT_CLASS = '.el-statistic__content' - -describe('Countdown.vue', () => { - it('render test', async () => { - const wrapper = mount(() => ( - - )) - - expect(wrapper.find(TITLE_CLASS).text()).toBe('test') - expect(wrapper.find(CONTENT_CLASS).text()).toBe('00:00:59') - }) - - describe('format', () => { - const value = dayjs() - .add(2, 'd') - .add(2, 'h') - .add(2, 'm') - .add(2, 's') - .add(2, 'ms') - - it.each([ - ['DD HH:mm:ss', '02 02:02:01'], - ['HH:mm:ss', '50:02:01'], - ['H:m:s', '50:2:1'], - ])('should work with %s', async (format, expected) => { - const wrapper = mount(() => ) - - expect(wrapper.find(CONTENT_CLASS).text()).toBe(expected) - }) - }) - - it('change event', async () => { - vi.useFakeTimers() - const onChange = vi.fn() - mount(() => ( - - )) - - vi.advanceTimersByTime((1000 / 30) * 2) - expect(onChange).toHaveBeenCalledTimes(2) - vi.useRealTimers() - }) - - it('finish event', async () => { - vi.useFakeTimers() - const onFinish = vi.fn() - mount(() => ( - - )) - - vi.advanceTimersByTime(1000 * 30) - expect(onFinish).not.toHaveBeenCalled() - vi.runAllTimers() - expect(onFinish).toHaveBeenCalled() - vi.useRealTimers() - }) -}) diff --git a/packages/components/statistic/index.ts b/packages/components/statistic/index.ts index d204d92a71..1c0c4995ed 100644 --- a/packages/components/statistic/index.ts +++ b/packages/components/statistic/index.ts @@ -1,11 +1,7 @@ import { withInstall } from '@element-plus/utils' import Statistic from './src/statistic.vue' -import Countdown from './src/countdown.vue' - -export * from './src/statistic' -export * from './src/countdown' export const ElStatistic = withInstall(Statistic) -export const ElCountdown = withInstall(Countdown) export default ElStatistic +export * from './src/statistic' diff --git a/packages/components/statistic/src/countdown.ts b/packages/components/statistic/src/countdown.ts deleted file mode 100644 index 83b391af8e..0000000000 --- a/packages/components/statistic/src/countdown.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { padStart } from 'lodash-unified' -import { buildProps, definePropType, isNumber } from '@element-plus/utils' -import { CHANGE_EVENT } from '@element-plus/constants' -import type { StyleValue } from 'vue' -import type { Dayjs } from 'dayjs' -import type Countdown from './countdown.vue' -export const countdownProps = buildProps({ - /** - * @description Formatting the countdown display - */ - format: { - type: String, - default: 'HH:mm:ss', - }, - /** - * @description Sets the prefix of a countdown - */ - prefix: String, - /** - * @description Sets the suffix of a countdown - */ - suffix: String, - /** - * @description countdown titles - */ - title: String, - /** - * @description target time - */ - value: { - type: definePropType([Number, Object]), - default: 0, - }, - /** - * @description Styles countdown values - */ - valueStyle: { - type: definePropType([String, Object, Array]), - }, -} as const) - -export const countdownEmits = { - finish: () => true, - [CHANGE_EVENT]: (value: number) => isNumber(value), -} - -export const formatTimeStr = (format: string, time: number) => { - 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 - ] - // The unformatted value of the previous tick - let pre = 0 - // previous tick - let preTick = 0 - // time left - let timeLeft = time - return timeUnits.reduce((con: string, item: [string, number]) => { - const name = item[0] - return con.replace(new RegExp(`${name}+`, 'g'), (match) => { - let sum = 0 - if (!format.includes(name)) { - pre = Math.floor(timeLeft / item[1]) - timeLeft = timeLeft - pre * item[1] - preTick = item[1] - } else { - pre = 0 - preTick = 0 - sum = Math.floor(timeLeft / item[1]) - timeLeft = timeLeft - sum * item[1] - if (pre > 0) { - sum = sum + pre * (preTick / item[1]) - pre = 0 - } - } - return padStart(String(sum), match.length, '0') // autoCompletion - }) - }, format) -} - -export type CountdownInstance = InstanceType diff --git a/packages/components/statistic/src/countdown.vue b/packages/components/statistic/src/countdown.vue deleted file mode 100644 index 5f489255e9..0000000000 --- a/packages/components/statistic/src/countdown.vue +++ /dev/null @@ -1,93 +0,0 @@ - - diff --git a/packages/components/statistic/src/statistic.ts b/packages/components/statistic/src/statistic.ts index 3265b787a2..993263f0ef 100644 --- a/packages/components/statistic/src/statistic.ts +++ b/packages/components/statistic/src/statistic.ts @@ -1,6 +1,9 @@ import { buildProps, definePropType } from '@element-plus/utils' -import type { StyleValue } from 'vue' + +import type { ExtractPropTypes, StyleValue } from 'vue' +import type { Dayjs } from 'dayjs' import type Statistic from './statistic.vue' + export const statisticProps = buildProps({ /** * @description Setting the decimal point @@ -26,14 +29,12 @@ export const statisticProps = buildProps({ /** * @description Custom numerical presentation */ - formatter: { - type: definePropType<(val: string | number) => string | number>(Function), - }, + formatter: Function, /** * @description Numerical content */ value: { - type: [String, Number], + type: definePropType([Number, Object]), default: 0, }, /** @@ -55,17 +56,7 @@ export const statisticProps = buildProps({ valueStyle: { type: definePropType([String, Object, Array]), }, - // rate: { - // type: Number, - // default: 3, - // }, } as const) -export const groupFormat = function ( - target: string, - step = 3, - groupSeparator = ',' -): string { - const reg = new RegExp(`\\B(?=(\\d{${step}})+(?!\\d))`, 'g') - return target.replace(reg, groupSeparator) -} +export type StatisticProps = ExtractPropTypes + export type StatisticInstance = InstanceType diff --git a/packages/components/statistic/src/statistic.vue b/packages/components/statistic/src/statistic.vue index 81e1908fbb..6749c3d4c7 100644 --- a/packages/components/statistic/src/statistic.vue +++ b/packages/components/statistic/src/statistic.vue @@ -1,6 +1,6 @@