From 3b77b1195199f74b06571dad457500dd56285202 Mon Sep 17 00:00:00 2001 From: zzk <974758671@qq.com> Date: Thu, 9 Nov 2023 21:08:44 +0800 Subject: [PATCH 1/6] feat(statistic): new component Statistic --- site/site.config.mjs | 8 + src/index.ts | 1 + src/statistic/Statistic.tsx | 175 +++++++++++++++++++++ src/statistic/__tests__/statistic.test.tsx | 126 +++++++++++++++ src/statistic/_example/animation.tsx | 33 ++++ src/statistic/_example/base.tsx | 11 ++ src/statistic/_example/color.tsx | 14 ++ src/statistic/_example/combination.tsx | 55 +++++++ src/statistic/_example/loading.tsx | 14 ++ src/statistic/_example/slot.tsx | 18 +++ src/statistic/_example/trend.tsx | 12 ++ src/statistic/defaultProps.ts | 12 ++ src/statistic/index.ts | 8 + src/statistic/statistic.en-US.md | 25 +++ src/statistic/statistic.md | 25 +++ src/statistic/style/css.js | 1 + src/statistic/style/index.js | 1 + src/statistic/tween.ts | 111 +++++++++++++ src/statistic/type.ts | 79 ++++++++++ 19 files changed, 729 insertions(+) create mode 100644 src/statistic/Statistic.tsx create mode 100644 src/statistic/__tests__/statistic.test.tsx create mode 100644 src/statistic/_example/animation.tsx create mode 100644 src/statistic/_example/base.tsx create mode 100644 src/statistic/_example/color.tsx create mode 100644 src/statistic/_example/combination.tsx create mode 100644 src/statistic/_example/loading.tsx create mode 100644 src/statistic/_example/slot.tsx create mode 100644 src/statistic/_example/trend.tsx create mode 100644 src/statistic/defaultProps.ts create mode 100644 src/statistic/index.ts create mode 100644 src/statistic/statistic.en-US.md create mode 100644 src/statistic/statistic.md create mode 100644 src/statistic/style/css.js create mode 100644 src/statistic/style/index.js create mode 100644 src/statistic/tween.ts create mode 100644 src/statistic/type.ts diff --git a/site/site.config.mjs b/site/site.config.mjs index c1e4e97aa3..d3fbf60cda 100644 --- a/site/site.config.mjs +++ b/site/site.config.mjs @@ -505,6 +505,14 @@ export const docs = [ component: () => import('tdesign-react/skeleton/skeleton.md'), componentEn: () => import('tdesign-react/skeleton/skeleton.en-US.md'), }, + { + title: 'Statistic 统计数值', + titleEn: 'Statistic', + name: 'statistic', + path: '/react/components/statistic', + component: () => import('tdesign-react/statistic/statistic.md'), + componentEn: () => import('tdesign-react/statistic/statistic.en-US.md'), + }, { title: 'Swiper 轮播框', titleEn: 'Swiper', diff --git a/src/index.ts b/src/index.ts index 0a7ab6af08..f654956689 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,3 +64,4 @@ export * from './rate'; export * from './link'; export * from './guide'; export * from './back-top'; +export * from './statistic'; diff --git a/src/statistic/Statistic.tsx b/src/statistic/Statistic.tsx new file mode 100644 index 0000000000..91ff608959 --- /dev/null +++ b/src/statistic/Statistic.tsx @@ -0,0 +1,175 @@ +import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import isNumber from 'lodash/isNumber'; +import isFunction from 'lodash/isFunction'; +import { + ArrowTriangleDownFilledIcon as TDArrowTriangleDownFilledIcon, + ArrowTriangleUpFilledIcon as TDArrowTriangleUpFilledIcon, +} from 'tdesign-icons-react'; +import { TdStatisticProps } from './type'; +import { statisticDefaultProps } from './defaultProps'; +import { StyledProps } from '../common'; +import useConfig from '../hooks/useConfig'; +import useGlobalIcon from '../hooks/useGlobalIcon'; +import useDefaultProps from '../hooks/useDefaultProps'; + +import Skeleton from '../skeleton'; +import Tween from './tween'; + +export interface StatisticProps extends TdStatisticProps, StyledProps {} + +export interface StatisticRef { + start: (from?: number, to?: number) => void; +} + +const Statistic = forwardRef((props, ref) => { + const { + animation, + animationStart, + color, + decimalPlaces, + extra, + format, + loading, + prefix, + separator, + suffix, + title, + trend, + trendPlacement, + unit, + value, + } = useDefaultProps(props, statisticDefaultProps); + const { classPrefix } = useConfig(); + const { ArrowTriangleUpFilledIcon } = useGlobalIcon({ ArrowTriangleUpFilledIcon: TDArrowTriangleUpFilledIcon }); + const { ArrowTriangleDownFilledIcon } = useGlobalIcon({ + ArrowTriangleDownFilledIcon: TDArrowTriangleDownFilledIcon, + }); + + /** + * init value + */ + const [innerValue, setInnerValue] = useState(animation?.valueFrom ?? value); + const numberValue = useMemo(() => (isNumber(value) ? value : 0), [value]); + + const tween = useRef(null); + + const start = (from: number = animation?.valueFrom ?? 0, to: number = numberValue) => { + if (from !== to) { + tween.current = new Tween({ + from: { + value: from, + }, + to: { + value: to, + }, + duration: props.animation.duration, + onUpdate: (keys) => { + setInnerValue(keys.value); + }, + onFinish: () => { + setInnerValue(to); + }, + }); + tween.current?.start(); + } + }; + + const formatValue = useMemo(() => { + // eslint-disable-next-line no-underscore-dangle + let _value: number | undefined | string = innerValue; + + if (isFunction(format)) { + return format(_value); + } + const options = { + minimumFractionDigits: decimalPlaces || 0, + maximumFractionDigits: decimalPlaces || 20, + useGrouping: !!separator, + }; + // replace的替换的方案仅能应对大部分地区 + _value = _value.toLocaleString(undefined, options).replace(/,|,/g, separator); + + return _value; + }, [innerValue, decimalPlaces, separator, format]); + + const COLOR_MAP = { + blue: 'var(--td-brand-color)', + red: 'var(--td-error-color)', + orange: 'var(--td-warning-color)', + green: 'var(--td-success-color)', + }; + + const valueStyle = useMemo( + () => ({ + color: COLOR_MAP[color] || color, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [color], + ); + + useEffect(() => { + animation && animationStart && start(); + return () => { + if (tween.current) { + tween.current.stop(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (animationStart && animation && !tween.current) { + start(); + } + return () => { + if (tween.current) { + tween.current.stop(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [animationStart]); + + useEffect(() => { + if (tween.current) { + tween.current?.stop(); + tween.current = null; + } + setInnerValue(value); + if (animationStart && animation) { + start(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + useImperativeHandle(ref, () => ({ + start, + })); + + const trendIcons = { + increase: , + decrease: , + }; + + const trendIcon = trend ? trendIcons[trend] : null; + + const prefixRender = prefix || (trendIcon && trendPlacement !== 'right' ? trendIcon : null); + const suffixRender = suffix || (trendIcon && trendPlacement === 'right' ? trendIcon : null); + + return ( +
+ {title &&
{title}
} + +
+ {prefixRender && {prefixRender}} + {formatValue} + {unit && {unit}} + {suffixRender && {suffixRender}} +
+
+ {extra &&
{extra}
} +
+ ); +}); + +Statistic.displayName = 'Statistic'; +export default Statistic; diff --git a/src/statistic/__tests__/statistic.test.tsx b/src/statistic/__tests__/statistic.test.tsx new file mode 100644 index 0000000000..ebe19e83a0 --- /dev/null +++ b/src/statistic/__tests__/statistic.test.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@test/utils'; +import { vi } from 'vitest'; +import { ArrowTriangleDownFilledIcon, ArrowTriangleUpFilledIcon } from 'tdesign-icons-react'; +import Statistic from '../index'; + +beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }); +}); + +afterEach(() => { + vi.runOnlyPendingTimers(); + vi.useRealTimers(); +}); + +describe('Statistic 组件测试', () => { + /** + * props + */ + + test('props', () => { + render(); + + expect(document.querySelector('.t-statistic-title')).toHaveTextContent('Total Assets'); + + expect(document.querySelector('.t-statistic-content-unit')).toHaveTextContent('%'); + }); + + /** + * color + */ + + const COLOR_MAP = { + black: 'black', + blue: 'var(--td-brand-color)', + red: 'var(--td-error-color)', + orange: 'var(--td-warning-color)', + green: 'var(--td-success-color)', + }; + const colors = ['black', 'blue', 'red', 'orange', 'green'] as const; + colors.forEach((color) => { + test('color', () => { + render(); + + expect(document.querySelector('.t-statistic-content')).toHaveStyle(`color: ${COLOR_MAP[color]}`); + }); + }); + + /** + * trend + */ + + test('trend', () => { + render( +
+ + +
, + ); + + const { container: upIcon } = render(); + const { container: downIcon } = render(); + + expect(upIcon).toBeInTheDocument(); + expect(downIcon).toBeInTheDocument(); + }); + + test('trendPlacement left', () => { + render(); + + expect(document.querySelector('.t-statistic-content-prefix')).toBeInTheDocument(); + }); + + test('trendPlacement right', () => { + render(); + + expect(document.querySelector('.t-statistic-content-suffix')).toBeInTheDocument(); + }); + + /** + * loading + */ + + test('loading', () => { + render(); + + expect(document.querySelector('.t-statistic-title')).toHaveTextContent('Total Assets'); + + expect(document.querySelector('.t-skeleton__row')).toBeInTheDocument(); + }); + + /** + * Start + */ + + test('Start Function', async () => { + const TestDom = () => { + const [start, setStart] = React.useState(false); + + return ( + <> + + +value.toFixed(2)} + animationStart={start} + /> + + ); + }; + render(); + + fireEvent.click(document.querySelector('#button')); + + vi.advanceTimersByTime(2000); + + await waitFor(() => { + expect(document.querySelector('.t-statistic-content-value')).toHaveTextContent('82.76'); + }); + }); +}); diff --git a/src/statistic/_example/animation.tsx b/src/statistic/_example/animation.tsx new file mode 100644 index 0000000000..271c251b32 --- /dev/null +++ b/src/statistic/_example/animation.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Space, Button, Statistic } from 'tdesign-react'; +import type { StatisticRef } from '../index'; + +const AnimationStatistic = () => { + const [start, setStart] = React.useState(false); + const [value, setValue] = React.useState(56.32); + const statisticRef = React.useRef(); + + return ( + + + + + + + + + ); +}; + +export default AnimationStatistic; diff --git a/src/statistic/_example/base.tsx b/src/statistic/_example/base.tsx new file mode 100644 index 0000000000..8c3572a2c5 --- /dev/null +++ b/src/statistic/_example/base.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Space, Statistic } from 'tdesign-react'; + +const BaseStatistic = () => ( + + + + +); + +export default BaseStatistic; diff --git a/src/statistic/_example/color.tsx b/src/statistic/_example/color.tsx new file mode 100644 index 0000000000..87542efba7 --- /dev/null +++ b/src/statistic/_example/color.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Space, Statistic } from 'tdesign-react'; + +const ColorStatistic = () => ( + + + + + + + +); + +export default ColorStatistic; diff --git a/src/statistic/_example/combination.tsx b/src/statistic/_example/combination.tsx new file mode 100644 index 0000000000..5d69d5910e --- /dev/null +++ b/src/statistic/_example/combination.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Space, Statistic, Card, Divider } from 'tdesign-react'; +import { IconFont } from 'tdesign-icons-react'; + +const CombinationStatistic = () => { + const iconStyle = { + fontSize: '32px', + color: '#0052d9', + background: '#f2f3ffff', + borderRadius: '6px', + padding: '12px', + }; + const separator = ; + return ( + + + + + + + + + + + + + + + + + the day before + + 9% + + + last week + + 9% + + + } + > + + + + + ); +}; + +export default CombinationStatistic; diff --git a/src/statistic/_example/loading.tsx b/src/statistic/_example/loading.tsx new file mode 100644 index 0000000000..8df40981ea --- /dev/null +++ b/src/statistic/_example/loading.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Space, Switch, Statistic } from 'tdesign-react'; + +const LoadingStatistic = () => { + const [loading, setLoading] = React.useState(false); + return ( + + setLoading(value)} size="large" /> + + + ); +}; + +export default LoadingStatistic; diff --git a/src/statistic/_example/slot.tsx b/src/statistic/_example/slot.tsx new file mode 100644 index 0000000000..b395ff6ac8 --- /dev/null +++ b/src/statistic/_example/slot.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Space, Statistic } from 'tdesign-react'; +import { ControlPlatformIcon, ArrowTriangleDownFilledIcon } from 'tdesign-icons-react'; + +const SlotStatistic = () => ( + + }> + + + } + > + +); + +export default SlotStatistic; diff --git a/src/statistic/_example/trend.tsx b/src/statistic/_example/trend.tsx new file mode 100644 index 0000000000..38cdb600b3 --- /dev/null +++ b/src/statistic/_example/trend.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Space, Statistic } from 'tdesign-react'; + +const TrendStatistic = () => ( + + + + + +); + +export default TrendStatistic; diff --git a/src/statistic/defaultProps.ts b/src/statistic/defaultProps.ts new file mode 100644 index 0000000000..f65d7a1076 --- /dev/null +++ b/src/statistic/defaultProps.ts @@ -0,0 +1,12 @@ +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdStatisticProps } from './type'; + +export const statisticDefaultProps: TdStatisticProps = { + animationStart: false, + loading: false, + separator: ',', + trendPlacement: 'left', +}; diff --git a/src/statistic/index.ts b/src/statistic/index.ts new file mode 100644 index 0000000000..c7cdfca0c1 --- /dev/null +++ b/src/statistic/index.ts @@ -0,0 +1,8 @@ +import _Statistic from './Statistic'; +import './style/index.js'; + +export type { StatisticProps, StatisticRef } from './Statistic'; +export * from './type'; + +export const Statistic = _Statistic; +export default Statistic; diff --git a/src/statistic/statistic.en-US.md b/src/statistic/statistic.en-US.md new file mode 100644 index 0000000000..0d53258f9a --- /dev/null +++ b/src/statistic/statistic.en-US.md @@ -0,0 +1,25 @@ +:: BASE_DOC :: + +## API + +### Statistic Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,Typescript:`React.CSSProperties` | N +animation | Object | - | Animation effect control, `duration` refers to the transition time of the animation `unit: millisecond`, `valueFrom` refers to the initial value of the animation. `{ duration, valueFrom }`。Typescript:`animation` `interface animation { duration: number; valueFrom: number; }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/statistic/type.ts) | N +animationStart | Boolean | false | Whether to start animation | N +color | String | - | Color style, followed by TDesign style black, blue, red, orange, green.Can also be any RGB equivalent supported by [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value)。options: black/blue/red/orange/green | N +decimalPlaces | Number | - | Decimal places | N +extra | TNode | - | Additional display content。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +format | Function | - | Format numeric display value。Typescript:`(value: number) => number` | N +loading | Boolean | false | Loading | N +prefix | TNode | - | Prefix content, display priority is higher than trend。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +separator | String | , | The carry separator is displayed by default, and can be customized to other content. When `separator = ''` is set to an empty string/null/undefined, the separator is hidden | N +suffix | TNode | - | Suffix content, display priority is higher than trend。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +title | TNode | - | The title of Statistic。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +trend | String | - | trend。options: increase/decrease | N +trendPlacement | String | left | Position of trending placements。options: left/right | N +unit | TNode | - | Unit content。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +value | Number | - | The value of Statistic | N diff --git a/src/statistic/statistic.md b/src/statistic/statistic.md new file mode 100644 index 0000000000..79700d3e5b --- /dev/null +++ b/src/statistic/statistic.md @@ -0,0 +1,25 @@ +:: BASE_DOC :: + +## API + +### Statistic Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +animation | Object | - | 动画效果控制,`duration` 指动画的过渡时间`单位:毫秒`,`valueFrom` 指动画的起始数值。`{ duration, valueFrom }`。TS 类型:`animation` `interface animation { duration: number; valueFrom: number; }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/statistic/type.ts) | N +animationStart | Boolean | false | 是否开始动画 | N +color | String | - | 颜色风格,依次为 TDesign 风格的黑色、蓝色、红色、橙色、绿色。也可以为任何 [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 支持的 RGB 等值。可选项:black/blue/red/orange/green | N +decimalPlaces | Number | - | 小数保留位数 | N +extra | TNode | - | 额外的显示内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +format | Function | - | 格式化数值显示值。TS 类型:`(value: number) => number` | N +loading | Boolean | false | 是否加载中 | N +prefix | TNode | - | 前缀内容,展示优先级高于 trend。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +separator | String | , | 默认展示进位分隔符,可以自定义为其他内容,`separator = ''` 设置为空字符串/null/undefined 时隐藏分隔符 | N +suffix | TNode | - | 后缀内容,展示优先级高于 trend。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +title | TNode | - | 数值显示的标题。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +trend | String | - | 趋势。可选项:increase/decrease | N +trendPlacement | String | left | 趋势展示位置。可选项:left/right | N +unit | TNode | - | 单位内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +value | Number | - | 数值显示的值 | N diff --git a/src/statistic/style/css.js b/src/statistic/style/css.js new file mode 100644 index 0000000000..6a9a4b1328 --- /dev/null +++ b/src/statistic/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/statistic/style/index.js b/src/statistic/style/index.js new file mode 100644 index 0000000000..4c0cc38c7d --- /dev/null +++ b/src/statistic/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/web/components/statistic/_index.less'; diff --git a/src/statistic/tween.ts b/src/statistic/tween.ts new file mode 100644 index 0000000000..fa9124074e --- /dev/null +++ b/src/statistic/tween.ts @@ -0,0 +1,111 @@ +import noop from '../_util/noop'; + +interface TweenSettings { + from: Record; + to: Record; + duration?: number; + delay?: number; + onStart?: (keys: Record) => void; + onUpdate?: (keys: Record) => void; + onFinish?: (keys: Record) => void; +} +const quartOut = (t: number) => 1 - Math.abs((t - 1) ** 4); + +export default class Tween { + private from: Record; + + private to: Record; + + private duration: number; + + private delay: number; + + private onStart?: (keys: Record) => void; + + private onUpdate: (keys: Record) => void; + + private onFinish?: (keys: Record) => void; + + private startTime: number; + + private started: boolean; + + private finished: boolean; + + private timer: number | null; + + private keys: Record; + + constructor({ from, to, duration = 500, delay = 0, onStart, onUpdate = noop, onFinish }: TweenSettings) { + this.from = from; + this.to = to; + this.duration = duration; + this.delay = delay; + this.onStart = onStart; + this.onUpdate = onUpdate; + this.onFinish = onFinish; + this.startTime = Date.now() + delay; + this.started = false; + this.finished = false; + this.timer = null; + this.keys = {}; + Object.entries(from).forEach(([key, value]) => { + if (this.to[key] === undefined) { + this.to[key] = value; + } + }); + + Object.entries(to).forEach(([key, value]) => { + if (this.from[key] === undefined) { + this.from[key] = value; + } + }); + } + + private time = 0; + + private elapsed = 0; + + private update() { + this.time = Date.now(); + if (this.time < this.startTime || this.finished) return; + + if (this.elapsed === this.duration) { + this.finished = true; + this.onFinish?.(this.keys); + return; + } + this.elapsed = Math.min(this.time - this.startTime, this.duration); + const progress = quartOut(this.elapsed / this.duration); + + Object.keys(this.to).forEach((key) => { + const delta = this.to[key] - this.from[key]; + this.keys[key] = this.from[key] + delta * progress; + }); + + if (!this.started) { + this.onStart?.(this.keys); + this.started = true; + } + + this.onUpdate(this.keys); + } + + public start() { + this.startTime = Date.now() + this.delay; + const tick = () => { + this.update(); + this.timer = requestAnimationFrame(tick); + if (this.finished) { + cancelAnimationFrame(this.timer); + this.timer = null; + } + }; + tick(); + } + + public stop() { + cancelAnimationFrame(this.timer); + this.timer = null; + } +} diff --git a/src/statistic/type.ts b/src/statistic/type.ts new file mode 100644 index 0000000000..64f868a4a7 --- /dev/null +++ b/src/statistic/type.ts @@ -0,0 +1,79 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TNode } from '../common'; + +export interface TdStatisticProps { + /** + * 动画效果控制,`duration` 指动画的过渡时间`单位:毫秒`,`valueFrom` 指动画的起始数值。`{ duration, valueFrom }` + */ + animation?: animation; + /** + * 是否开始动画 + * @default false + */ + animationStart?: boolean; + /** + * 颜色风格,依次为 TDesign 风格的黑色、蓝色、红色、橙色、绿色。也可以为任何 [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 支持的 RGB 等值 + */ + color?: 'black' | 'blue' | 'red' | 'orange' | 'green'; + /** + * 小数保留位数 + */ + decimalPlaces?: number; + /** + * 额外的显示内容 + */ + extra?: TNode; + /** + * 格式化数值显示值 + */ + format?: (value: number) => number; + /** + * 是否加载中 + * @default false + */ + loading?: boolean; + /** + * 前缀内容,展示优先级高于 trend + */ + prefix?: TNode; + /** + * 默认展示进位分隔符,可以自定义为其他内容,`separator = ''` 设置为空字符串/null/undefined 时隐藏分隔符 + * @default , + */ + separator?: string; + /** + * 后缀内容,展示优先级高于 trend + */ + suffix?: TNode; + /** + * 数值显示的标题 + */ + title?: TNode; + /** + * 趋势 + */ + trend?: 'increase' | 'decrease'; + /** + * 趋势展示位置 + * @default left + */ + trendPlacement?: 'left' | 'right'; + /** + * 单位内容 + */ + unit?: TNode; + /** + * 数值显示的值 + */ + value?: number; +} + +export interface animation { + duration: number; + valueFrom: number; +} From 110077e7adb7cf8096f1c3c0111f22b95b44fe2d Mon Sep 17 00:00:00 2001 From: zzk <974758671@qq.com> Date: Wed, 15 Nov 2023 22:29:11 +0800 Subject: [PATCH 2/6] test(statistic): update test --- src/statistic/Statistic.tsx | 11 +++++------ src/statistic/_example/combination.tsx | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/statistic/Statistic.tsx b/src/statistic/Statistic.tsx index 91ff608959..b16be72f13 100644 --- a/src/statistic/Statistic.tsx +++ b/src/statistic/Statistic.tsx @@ -118,9 +118,8 @@ const Statistic = forwardRef((props, ref) => { }, []); useEffect(() => { - if (animationStart && animation && !tween.current) { - start(); - } + animationStart && animation && !tween.current && start(); + return () => { if (tween.current) { tween.current.stop(); @@ -135,9 +134,9 @@ const Statistic = forwardRef((props, ref) => { tween.current = null; } setInnerValue(value); - if (animationStart && animation) { - start(); - } + + animationStart && animation && start(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); diff --git a/src/statistic/_example/combination.tsx b/src/statistic/_example/combination.tsx index 5d69d5910e..8201e69bd9 100644 --- a/src/statistic/_example/combination.tsx +++ b/src/statistic/_example/combination.tsx @@ -5,9 +5,9 @@ import { IconFont } from 'tdesign-icons-react'; const CombinationStatistic = () => { const iconStyle = { fontSize: '32px', - color: '#0052d9', - background: '#f2f3ffff', - borderRadius: '6px', + color: 'var(--td-brand-color)', + background: 'var(--td-brand-color-light)', + borderRadius: 'var(--td-radius-medium)', padding: '12px', }; const separator = ; From 1091b06865a5a30fb75b5a8d7b28da34a58d7949 Mon Sep 17 00:00:00 2001 From: Uyarn Date: Thu, 16 Nov 2023 17:04:22 +0800 Subject: [PATCH 3/6] chore: update snapshot --- src/_common | 2 +- src/statistic/Statistic.tsx | 10 +- .../_example/{animation.tsx => animation.jsx} | 3 +- src/statistic/_example/{base.tsx => base.jsx} | 0 .../_example/{color.tsx => color.jsx} | 0 .../{combination.tsx => combination.jsx} | 0 .../_example/{loading.tsx => loading.jsx} | 0 src/statistic/_example/{slot.tsx => slot.jsx} | 0 .../_example/{trend.tsx => trend.jsx} | 0 src/statistic/tween.ts | 111 - test/snap/__snapshots__/csr.test.jsx.snap | 2485 +++++++++++++++++ test/snap/__snapshots__/ssr.test.jsx.snap | 14 + 12 files changed, 2503 insertions(+), 122 deletions(-) rename src/statistic/_example/{animation.tsx => animation.jsx} (89%) rename src/statistic/_example/{base.tsx => base.jsx} (100%) rename src/statistic/_example/{color.tsx => color.jsx} (100%) rename src/statistic/_example/{combination.tsx => combination.jsx} (100%) rename src/statistic/_example/{loading.tsx => loading.jsx} (100%) rename src/statistic/_example/{slot.tsx => slot.jsx} (100%) rename src/statistic/_example/{trend.tsx => trend.jsx} (100%) delete mode 100644 src/statistic/tween.ts diff --git a/src/_common b/src/_common index b62cac6f5d..117e55b1b9 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit b62cac6f5d75b1af5b527a2849bedee42e906d6d +Subproject commit 117e55b1b96eee7995c7c7afc7537801dc9878cb diff --git a/src/statistic/Statistic.tsx b/src/statistic/Statistic.tsx index b16be72f13..cd7fbe31b7 100644 --- a/src/statistic/Statistic.tsx +++ b/src/statistic/Statistic.tsx @@ -13,7 +13,8 @@ import useGlobalIcon from '../hooks/useGlobalIcon'; import useDefaultProps from '../hooks/useDefaultProps'; import Skeleton from '../skeleton'; -import Tween from './tween'; +import Tween from '../_common/js/statistic/tween'; +import { COLOR_MAP } from '../_common/js/statistic/utils'; export interface StatisticProps extends TdStatisticProps, StyledProps {} @@ -92,13 +93,6 @@ const Statistic = forwardRef((props, ref) => { return _value; }, [innerValue, decimalPlaces, separator, format]); - const COLOR_MAP = { - blue: 'var(--td-brand-color)', - red: 'var(--td-error-color)', - orange: 'var(--td-warning-color)', - green: 'var(--td-success-color)', - }; - const valueStyle = useMemo( () => ({ color: COLOR_MAP[color] || color, diff --git a/src/statistic/_example/animation.tsx b/src/statistic/_example/animation.jsx similarity index 89% rename from src/statistic/_example/animation.tsx rename to src/statistic/_example/animation.jsx index 271c251b32..1d96cc47e8 100644 --- a/src/statistic/_example/animation.tsx +++ b/src/statistic/_example/animation.jsx @@ -1,11 +1,10 @@ import React from 'react'; import { Space, Button, Statistic } from 'tdesign-react'; -import type { StatisticRef } from '../index'; const AnimationStatistic = () => { const [start, setStart] = React.useState(false); const [value, setValue] = React.useState(56.32); - const statisticRef = React.useRef(); + const statisticRef = React.useRef(); return ( diff --git a/src/statistic/_example/base.tsx b/src/statistic/_example/base.jsx similarity index 100% rename from src/statistic/_example/base.tsx rename to src/statistic/_example/base.jsx diff --git a/src/statistic/_example/color.tsx b/src/statistic/_example/color.jsx similarity index 100% rename from src/statistic/_example/color.tsx rename to src/statistic/_example/color.jsx diff --git a/src/statistic/_example/combination.tsx b/src/statistic/_example/combination.jsx similarity index 100% rename from src/statistic/_example/combination.tsx rename to src/statistic/_example/combination.jsx diff --git a/src/statistic/_example/loading.tsx b/src/statistic/_example/loading.jsx similarity index 100% rename from src/statistic/_example/loading.tsx rename to src/statistic/_example/loading.jsx diff --git a/src/statistic/_example/slot.tsx b/src/statistic/_example/slot.jsx similarity index 100% rename from src/statistic/_example/slot.tsx rename to src/statistic/_example/slot.jsx diff --git a/src/statistic/_example/trend.tsx b/src/statistic/_example/trend.jsx similarity index 100% rename from src/statistic/_example/trend.tsx rename to src/statistic/_example/trend.jsx diff --git a/src/statistic/tween.ts b/src/statistic/tween.ts deleted file mode 100644 index fa9124074e..0000000000 --- a/src/statistic/tween.ts +++ /dev/null @@ -1,111 +0,0 @@ -import noop from '../_util/noop'; - -interface TweenSettings { - from: Record; - to: Record; - duration?: number; - delay?: number; - onStart?: (keys: Record) => void; - onUpdate?: (keys: Record) => void; - onFinish?: (keys: Record) => void; -} -const quartOut = (t: number) => 1 - Math.abs((t - 1) ** 4); - -export default class Tween { - private from: Record; - - private to: Record; - - private duration: number; - - private delay: number; - - private onStart?: (keys: Record) => void; - - private onUpdate: (keys: Record) => void; - - private onFinish?: (keys: Record) => void; - - private startTime: number; - - private started: boolean; - - private finished: boolean; - - private timer: number | null; - - private keys: Record; - - constructor({ from, to, duration = 500, delay = 0, onStart, onUpdate = noop, onFinish }: TweenSettings) { - this.from = from; - this.to = to; - this.duration = duration; - this.delay = delay; - this.onStart = onStart; - this.onUpdate = onUpdate; - this.onFinish = onFinish; - this.startTime = Date.now() + delay; - this.started = false; - this.finished = false; - this.timer = null; - this.keys = {}; - Object.entries(from).forEach(([key, value]) => { - if (this.to[key] === undefined) { - this.to[key] = value; - } - }); - - Object.entries(to).forEach(([key, value]) => { - if (this.from[key] === undefined) { - this.from[key] = value; - } - }); - } - - private time = 0; - - private elapsed = 0; - - private update() { - this.time = Date.now(); - if (this.time < this.startTime || this.finished) return; - - if (this.elapsed === this.duration) { - this.finished = true; - this.onFinish?.(this.keys); - return; - } - this.elapsed = Math.min(this.time - this.startTime, this.duration); - const progress = quartOut(this.elapsed / this.duration); - - Object.keys(this.to).forEach((key) => { - const delta = this.to[key] - this.from[key]; - this.keys[key] = this.from[key] + delta * progress; - }); - - if (!this.started) { - this.onStart?.(this.keys); - this.started = true; - } - - this.onUpdate(this.keys); - } - - public start() { - this.startTime = Date.now() + this.delay; - const tick = () => { - this.update(); - this.timer = requestAnimationFrame(tick); - if (this.finished) { - cancelAnimationFrame(this.timer); - this.timer = null; - } - }; - tick(); - } - - public stop() { - cancelAnimationFrame(this.timer); - this.timer = null; - } -} diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 2178b554ab..cfea0ffecb 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -179372,6 +179372,2491 @@ exports[`csr snapshot test > csr test src/space/_example/vertical.jsx 1`] = ` } `; +exports[`csr snapshot test > csr test src/statistic/_example/animation.jsx 1`] = ` +{ + "asFragment": [Function], + "baseElement": +