This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import * as React from 'react';
import type { StatisticProps } from './Statistic';
import type { valueType } from './utils';
export interface CountdownProps extends StatisticProps {
format?: string;
onFinish?: () => void;
onChange?: (value?: valueType) => void;
}
declare const _default: React.NamedExoticComponent<CountdownProps>;
export default _default;
+16
View File
@@ -0,0 +1,16 @@
"use client";
import * as React from 'react';
import { devUseWarning } from '../_util/warning';
import StatisticTimer from './Timer';
const Countdown = props => {
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Countdown');
warning.deprecated(false, '<Statistic.Countdown />', '<Statistic.Timer type="countdown" />');
}
return /*#__PURE__*/React.createElement(StatisticTimer, {
...props,
type: "countdown"
});
};
export default /*#__PURE__*/React.memo(Countdown);
+8
View File
@@ -0,0 +1,8 @@
import * as React from 'react';
import type { FormatConfig, valueType } from './utils';
interface NumberProps extends FormatConfig {
value: valueType;
prefixCls?: string;
}
declare const StatisticNumber: React.FC<NumberProps>;
export default StatisticNumber;
+49
View File
@@ -0,0 +1,49 @@
"use client";
import * as React from 'react';
import { isNumber } from '../_util/is';
const StatisticNumber = props => {
const {
value,
formatter,
precision,
decimalSeparator,
groupSeparator = '',
prefixCls
} = props;
let valueNode;
if (typeof formatter === 'function') {
// Customize formatter
valueNode = formatter(value);
} else {
// Internal formatter
const val = String(value);
const cells = val.match(/^(-?)(\d*)(\.(\d+))?$/);
// Process if illegal number
if (!cells || val === '-') {
valueNode = val;
} else {
const negative = cells[1];
let int = cells[2] || '0';
let decimal = cells[4] || '';
int = int.replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator);
if (isNumber(precision)) {
decimal = decimal.padEnd(precision, '0').slice(0, precision > 0 ? precision : 0);
}
if (decimal) {
decimal = `${decimalSeparator}${decimal}`;
}
valueNode = [/*#__PURE__*/React.createElement("span", {
key: "int",
className: `${prefixCls}-content-value-int`
}, negative, int), decimal && (/*#__PURE__*/React.createElement("span", {
key: "decimal",
className: `${prefixCls}-content-value-decimal`
}, decimal))];
}
}
return /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-content-value`
}, valueNode);
};
export default StatisticNumber;
+49
View File
@@ -0,0 +1,49 @@
import * as React from 'react';
import type { HTMLAriaDataAttributes } from '../_util/aria-data-attrs';
import type { SemanticClassNamesType, SemanticStylesType } from '../_util/hooks';
import type { FormatConfig, valueType } from './utils';
export type StatisticSemanticName = keyof StatisticSemanticClassNames & keyof StatisticSemanticStyles;
export type StatisticSemanticClassNames = {
root?: string;
content?: string;
title?: string;
header?: string;
prefix?: string;
suffix?: string;
};
export type StatisticSemanticStyles = {
root?: React.CSSProperties;
content?: React.CSSProperties;
title?: React.CSSProperties;
header?: React.CSSProperties;
prefix?: React.CSSProperties;
suffix?: React.CSSProperties;
};
export type StatisticClassNamesType = SemanticClassNamesType<StatisticProps, StatisticSemanticClassNames>;
export type StatisticStylesType = SemanticStylesType<StatisticProps, StatisticSemanticStyles>;
export interface StatisticRef {
nativeElement: HTMLDivElement;
}
interface StatisticReactProps extends FormatConfig {
prefixCls?: string;
className?: string;
classNames?: StatisticClassNamesType;
styles?: StatisticStylesType;
rootClassName?: string;
style?: React.CSSProperties;
value?: valueType;
/** @deprecated Please use `styles.content` instead */
valueStyle?: React.CSSProperties;
valueRender?: (node: React.ReactNode) => React.ReactNode;
title?: React.ReactNode;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
loading?: boolean;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
}
export type StatisticProps = HTMLAriaDataAttributes & StatisticReactProps;
declare const Statistic: React.ForwardRefExoticComponent<React.AriaAttributes & {
[key: `data-${string}`]: unknown;
} & Pick<React.HTMLAttributes<HTMLDivElement>, "role"> & StatisticReactProps & React.RefAttributes<StatisticRef>>;
export default Statistic;
+127
View File
@@ -0,0 +1,127 @@
"use client";
import * as React from 'react';
import pickAttrs from "@rc-component/util/es/pickAttrs";
import { clsx } from 'clsx';
import { useMergeSemantic } from '../_util/hooks';
import { devUseWarning } from '../_util/warning';
import { useComponentConfig } from '../config-provider/context';
import Skeleton from '../skeleton';
import StatisticNumber from './Number';
import useStyle from './style';
const Statistic = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls: customizePrefixCls,
className,
rootClassName,
style,
valueStyle,
value = 0,
title,
valueRender,
prefix,
suffix,
loading = false,
/* --- FormatConfig starts --- */
formatter,
precision,
decimalSeparator = '.',
groupSeparator = ',',
/* --- FormatConfig starts --- */
onMouseEnter,
onMouseLeave,
styles,
classNames,
...rest
} = props;
const {
getPrefixCls,
direction,
className: contextClassName,
style: contextStyle,
classNames: contextClassNames,
styles: contextStyles
} = useComponentConfig('statistic');
const prefixCls = getPrefixCls('statistic', customizePrefixCls);
const [hashId, cssVarCls] = useStyle(prefixCls);
const mergedProps = {
...props,
decimalSeparator,
groupSeparator,
loading,
value
};
const [mergedClassNames, mergedStyles] = useMergeSemantic([contextClassNames, classNames], [contextStyles, styles], {
props: mergedProps
});
// ============================= Warning ==============================
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Statistic');
[['valueStyle', 'styles.content']].forEach(([deprecatedName, newName]) => {
warning.deprecated(!(deprecatedName in props), deprecatedName, newName);
});
}
const valueNode = /*#__PURE__*/React.createElement(StatisticNumber, {
decimalSeparator: decimalSeparator,
groupSeparator: groupSeparator,
prefixCls: prefixCls,
formatter: formatter,
precision: precision,
value: value
});
const rootClassNames = clsx(prefixCls, {
[`${prefixCls}-rtl`]: direction === 'rtl'
}, contextClassName, className, rootClassName, mergedClassNames.root, hashId, cssVarCls);
const headerClassNames = clsx(`${prefixCls}-header`, mergedClassNames.header);
const titleClassNames = clsx(`${prefixCls}-title`, mergedClassNames.title);
const contentClassNames = clsx(`${prefixCls}-content`, mergedClassNames.content);
const prefixClassNames = clsx(`${prefixCls}-content-prefix`, mergedClassNames.prefix);
const suffixClassNames = clsx(`${prefixCls}-content-suffix`, mergedClassNames.suffix);
const internalRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
nativeElement: internalRef.current
}));
const restProps = pickAttrs(rest, {
aria: true,
data: true
});
return /*#__PURE__*/React.createElement("div", {
...restProps,
className: rootClassNames,
style: {
...mergedStyles.root,
...contextStyle,
...style
},
ref: internalRef,
onMouseEnter: onMouseEnter,
onMouseLeave: onMouseLeave
}, title && (/*#__PURE__*/React.createElement("div", {
className: headerClassNames,
style: mergedStyles.header
}, /*#__PURE__*/React.createElement("div", {
className: titleClassNames,
style: mergedStyles.title
}, title))), /*#__PURE__*/React.createElement(Skeleton, {
paragraph: false,
loading: loading,
className: `${prefixCls}-skeleton`,
active: true
}, /*#__PURE__*/React.createElement("div", {
className: contentClassNames,
style: {
...valueStyle,
...mergedStyles.content
}
}, prefix && (/*#__PURE__*/React.createElement("span", {
className: prefixClassNames,
style: mergedStyles.prefix
}, prefix)), typeof valueRender === 'function' ? valueRender(valueNode) : valueNode, suffix && (/*#__PURE__*/React.createElement("span", {
className: suffixClassNames,
style: mergedStyles.suffix
}, suffix)))));
});
if (process.env.NODE_ENV !== 'production') {
Statistic.displayName = 'Statistic';
}
export default Statistic;
+15
View File
@@ -0,0 +1,15 @@
import * as React from 'react';
import type { StatisticProps } from './Statistic';
import type { FormatConfig, valueType } from './utils';
export type TimerType = 'countdown' | 'countup';
export interface StatisticTimerProps extends FormatConfig, StatisticProps {
type: TimerType;
format?: string;
/**
* Only to be called when the type is `countdown`.
*/
onFinish?: () => void;
onChange?: (value?: valueType) => void;
}
declare const StatisticTimer: React.FC<StatisticTimerProps>;
export default StatisticTimer;
+76
View File
@@ -0,0 +1,76 @@
"use client";
import * as React from 'react';
import { useEvent } from '@rc-component/util';
import { cloneElement } from '../_util/reactNode';
import Statistic from './Statistic';
import { formatCounter } from './utils';
const UPDATE_INTERVAL = 1000 / 60;
function getTime(value) {
return new Date(value).getTime();
}
const StatisticTimer = props => {
const {
value,
format = 'HH:mm:ss',
onChange,
onFinish,
type,
...rest
} = props;
const down = type === 'countdown';
// We reuse state here to do same as `forceUpdate`
const [showTime, setShowTime] = React.useState(null);
// ======================== Update ========================
const update = useEvent(() => {
const now = Date.now();
const timestamp = getTime(value);
setShowTime({});
const timeDiff = !down ? now - timestamp : timestamp - now;
onChange?.(timeDiff);
// Only countdown will trigger `onFinish`
if (down && timestamp < now) {
onFinish?.();
return false;
}
return true;
});
// Effect trigger
React.useEffect(() => {
let intervalId;
const tick = () => {
if (!update()) {
window.clearInterval(intervalId);
}
};
const startTimer = () => {
intervalId = window.setInterval(tick, UPDATE_INTERVAL);
};
const stopTimer = () => {
window.clearInterval(intervalId);
};
startTimer();
return () => {
stopTimer();
};
}, [value, down]);
React.useEffect(() => {
setShowTime({});
}, []);
// ======================== Format ========================
const formatter = (formatValue, config) => showTime ? formatCounter(formatValue, {
...config,
format
}, down) : '-';
const valueRender = node => cloneElement(node, {
title: undefined
});
// ======================== Render ========================
return /*#__PURE__*/React.createElement(Statistic, {
...rest,
value: value,
valueRender: valueRender,
formatter: formatter
});
};
export default StatisticTimer;
+17
View File
@@ -0,0 +1,17 @@
import type { CountdownProps } from './Countdown';
import Countdown from './Countdown';
import type { StatisticProps, StatisticSemanticClassNames, StatisticSemanticName, StatisticSemanticStyles } from './Statistic';
import Statistic from './Statistic';
import type { StatisticTimerProps } from './Timer';
import Timer from './Timer';
export type { CountdownProps, StatisticProps, StatisticSemanticClassNames, StatisticSemanticName, StatisticSemanticStyles, StatisticTimerProps, };
type CompoundedComponent = {
/**
* @deprecated Please use `Statistic.Timer` instead
*/
Countdown: typeof Countdown;
Timer: typeof Timer;
};
export type CompoundedStatistic = typeof Statistic & CompoundedComponent;
declare const _default: CompoundedStatistic;
export default _default;
+8
View File
@@ -0,0 +1,8 @@
"use client";
import Countdown from './Countdown';
import Statistic from './Statistic';
import Timer from './Timer';
Statistic.Timer = Timer;
Statistic.Countdown = Countdown;
export default Statistic;
+16
View File
@@ -0,0 +1,16 @@
import type { GetDefaultToken } from '../../theme/internal';
export interface ComponentToken {
/**
* @desc 标题字体大小
* @descEN Title font size
*/
titleFontSize: number;
/**
* @desc 内容字体大小
* @descEN Content font size
*/
contentFontSize: number;
}
export declare const prepareComponentToken: GetDefaultToken<'Statistic'>;
declare const _default: (prefixCls: string, rootCls?: string) => readonly [string, string];
export default _default;
+62
View File
@@ -0,0 +1,62 @@
import { resetComponent } from '../../style';
import { genStyleHooks, mergeToken } from '../../theme/internal';
const genStatisticStyle = token => {
const {
componentCls,
marginXXS,
padding,
colorTextDescription,
titleFontSize,
colorTextHeading,
contentFontSize,
fontFamily
} = token;
return {
[componentCls]: {
...resetComponent(token),
[`${componentCls}-header`]: {
paddingBottom: marginXXS,
[`${componentCls}-title`]: {
color: colorTextDescription,
fontSize: titleFontSize
}
},
[`${componentCls}-skeleton`]: {
paddingTop: padding
},
[`${componentCls}-content`]: {
color: colorTextHeading,
fontSize: contentFontSize,
fontFamily,
[`${componentCls}-content-value`]: {
display: 'inline-block',
direction: 'ltr'
},
[`${componentCls}-content-prefix, ${componentCls}-content-suffix`]: {
display: 'inline-block'
},
[`${componentCls}-content-prefix`]: {
marginInlineEnd: marginXXS
},
[`${componentCls}-content-suffix`]: {
marginInlineStart: marginXXS
}
}
}
};
};
// ============================== Export ==============================
export const prepareComponentToken = token => {
const {
fontSizeHeading3,
fontSize
} = token;
return {
titleFontSize: fontSize,
contentFontSize: fontSizeHeading3
};
};
export default genStyleHooks('Statistic', token => {
const statisticToken = mergeToken(token, {});
return genStatisticStyle(statisticToken);
}, prepareComponentToken);
+15
View File
@@ -0,0 +1,15 @@
import type * as React from 'react';
export type valueType = number | string;
export type countdownValueType = number | string;
export type Formatter = false | 'number' | 'countdown' | ((value: valueType, config?: FormatConfig) => React.ReactNode);
export interface FormatConfig {
formatter?: Formatter;
decimalSeparator?: string;
groupSeparator?: string;
precision?: number;
}
export interface CountdownFormatConfig extends FormatConfig {
format?: string;
}
export declare function formatTimeStr(duration: number, format: string): string;
export declare function formatCounter(value: valueType, config: CountdownFormatConfig, down: boolean): string;
+47
View File
@@ -0,0 +1,47 @@
// Countdown
const timeUnits = [['Y', 1000 * 60 * 60 * 24 * 365],
// years
['M', 1000 * 60 * 60 * 24 * 30],
// months
['D', 1000 * 60 * 60 * 24],
// days
['H', 1000 * 60 * 60],
// hours
['m', 1000 * 60],
// minutes
['s', 1000],
// seconds
['S', 1] // million seconds
];
export function formatTimeStr(duration, format) {
let leftDuration = duration;
const escapeRegex = /\[[^\]]*]/g;
const keepList = (format.match(escapeRegex) || []).map(str => str.slice(1, -1));
const templateText = format.replace(escapeRegex, '[]');
const replacedText = timeUnits.reduce((current, [name, unit]) => {
if (current.includes(name)) {
const value = Math.floor(leftDuration / unit);
leftDuration -= value * unit;
return current.replace(new RegExp(`${name}+`, 'g'), match => {
const len = match.length;
return value.toString().padStart(len, '0');
});
}
return current;
}, templateText);
let index = 0;
return replacedText.replace(escapeRegex, () => {
const match = keepList[index];
index += 1;
return match;
});
}
export function formatCounter(value, config, down) {
const {
format = ''
} = config;
const target = new Date(value).getTime();
const current = Date.now();
const diff = down ? Math.max(target - current, 0) : Math.max(current - target, 0);
return formatTimeStr(diff, format);
}