1
0

index.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import React, { ReactNode, Component } from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/progress/constants';
  5. import '@douyinfe/semi-foundation/progress/progress.scss';
  6. import { Animation } from '@douyinfe/semi-animation';
  7. import { Motion } from '../_base/base';
  8. const prefixCls = cssClasses.PREFIX;
  9. export interface ProgressProps {
  10. className?: string;
  11. direction?: 'horizontal' | 'vertical';
  12. format?: (percent: number) => React.ReactNode;
  13. motion?: Motion;
  14. orbitStroke?: string;
  15. percent?: number;
  16. showInfo?: boolean;
  17. size?: 'default' | 'small' | 'large';
  18. stroke?: string;
  19. strokeLinecap?: 'round' | 'square';
  20. strokeWidth?: number;
  21. style?: React.CSSProperties;
  22. type?: 'line' | 'circle';
  23. width?: number;
  24. }
  25. export interface ProgressState {
  26. percentNumber: number;
  27. }
  28. class Progress extends Component<ProgressProps, ProgressState> {
  29. static propTypes = {
  30. className: PropTypes.string,
  31. direction: PropTypes.oneOf(strings.directions),
  32. format: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
  33. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  34. orbitStroke: PropTypes.string,
  35. percent: PropTypes.number,
  36. scale: PropTypes.number,
  37. showInfo: PropTypes.bool,
  38. size: PropTypes.oneOf(strings.sizes),
  39. stroke: PropTypes.string,
  40. strokeLinecap: PropTypes.oneOf(strings.strokeLineCap),
  41. strokeWidth: PropTypes.number,
  42. style: PropTypes.object,
  43. type: PropTypes.oneOf(strings.types),
  44. width: PropTypes.number,
  45. };
  46. static defaultProps = {
  47. className: '',
  48. direction: strings.DEFAULT_DIRECTION,
  49. format: (text: string): string => `${text }%`,
  50. motion: true,
  51. orbitStroke: 'var(--semi-color-fill-0)',
  52. percent: 0,
  53. showInfo: false,
  54. size: strings.DEFAULT_SIZE,
  55. stroke: 'var(--semi-color-success)',
  56. strokeLinecap: strings.DEFAULT_LINECAP,
  57. strokeWidth: 4,
  58. style: {},
  59. type: strings.DEFAULT_TYPE,
  60. };
  61. _mounted: boolean = true;
  62. animation: Animation;
  63. constructor(props: ProgressProps) {
  64. super(props);
  65. this._mounted = true;
  66. this.state = {
  67. percentNumber: this.props.percent // Specially used for animation of numbers
  68. };
  69. }
  70. componentDidUpdate(prevProps: ProgressProps): void {
  71. if (isNaN(this.props.percent) || isNaN(prevProps.percent)) {
  72. throw new Error('[Semi Progress]:percent can not be NaN');
  73. return;
  74. }
  75. if (prevProps.percent !== this.props.percent) {
  76. if (!this.props.motion) {
  77. // eslint-disable-next-line
  78. this.setState({ percentNumber: this.props.percent });
  79. return;
  80. }
  81. if (this.animation && this.animation.destroy) {
  82. this.animation.destroy();
  83. }
  84. this.animation = new Animation({
  85. from: { value: prevProps.percent },
  86. to: { value: this.props.percent }
  87. }, {
  88. // easing: 'cubic-bezier(0, .68, .3, 1)'
  89. easing: 'linear',
  90. duration: 300
  91. });
  92. this.animation.on('frame', (props: any) => {
  93. // prevent setState while component is unmounted but this timer is called
  94. if (this._mounted === false) {
  95. return;
  96. }
  97. // let percentNumber = Number.isInteger(props.value) ? props.value : Math.floor(props.value * 100) / 100;
  98. const percentNumber = parseInt(props.value);
  99. this.setState({ percentNumber });
  100. });
  101. this.animation.on('rest', () => {
  102. // prevent setState while component is unmounted but this timer is called
  103. if (this._mounted === false) {
  104. return;
  105. }
  106. this.setState({ percentNumber: this.props.percent });
  107. });
  108. this.animation.start();
  109. }
  110. }
  111. componentWillUnmount(): void {
  112. this.animation && this.animation.destroy();
  113. this._mounted = false;
  114. }
  115. renderCircleProgress(): ReactNode {
  116. const { strokeLinecap, style, className, strokeWidth, format, size, stroke, showInfo, percent, orbitStroke } = this.props;
  117. const { percentNumber } = this.state;
  118. const classNames = {
  119. wrapper: cls(`${prefixCls }-circle`, className),
  120. svg: cls(`${prefixCls }-circle-ring`),
  121. circle: cls(`${prefixCls }-circle-ring-inner`)
  122. };
  123. const perc = this.calcPercent(percent);
  124. const percNumber = this.calcPercent(percentNumber);
  125. let width;
  126. if (this.props.width) {
  127. width = this.props.width;
  128. } else {
  129. size === strings.DEFAULT_SIZE ? width = 72 : width = 24;
  130. }
  131. // cx, cy is circle center
  132. const cy = width / 2;
  133. const cx = width / 2;
  134. const radius = (width - strokeWidth) / 2; // radius
  135. const circumference = radius * 2 * Math.PI;
  136. const strokeDashoffset = (1 - perc / 100) * circumference; // Offset
  137. const strokeDasharray = `${circumference} ${circumference}`;
  138. const text = format(percNumber);
  139. return (
  140. <div className={classNames.wrapper} style={style}>
  141. <svg className={classNames.svg} height={width} width={width}>
  142. <circle
  143. strokeDashoffset={0}
  144. strokeWidth={strokeWidth}
  145. strokeDasharray={strokeDasharray}
  146. strokeLinecap={strokeLinecap}
  147. fill="transparent"
  148. stroke={orbitStroke}
  149. r={radius}
  150. cx={cx}
  151. cy={cy}
  152. />
  153. <circle
  154. className={classNames.circle}
  155. strokeDashoffset={strokeDashoffset}
  156. strokeWidth={strokeWidth}
  157. strokeDasharray={strokeDasharray}
  158. strokeLinecap={strokeLinecap}
  159. fill="transparent"
  160. stroke={stroke}
  161. r={radius}
  162. cx={cx}
  163. cy={cy}
  164. />
  165. </svg>
  166. {showInfo && size !== 'small' ? (<span className={`${prefixCls }-circle-text`}>{text}</span>) : null}
  167. </div>
  168. );
  169. }
  170. calcPercent(percent: number): number {
  171. let perc;
  172. if (percent > 100) {
  173. perc = 100;
  174. } else if (percent < 0) {
  175. perc = 0;
  176. } else {
  177. perc = percent;
  178. }
  179. return perc;
  180. }
  181. renderLineProgress(): ReactNode {
  182. const { className, style, stroke, direction, format, showInfo, size, percent, orbitStroke } = this.props;
  183. const { percentNumber } = this.state;
  184. const progressWrapperCls = cls(prefixCls, className, {
  185. [`${prefixCls }-horizontal`]: direction === strings.DEFAULT_DIRECTION,
  186. [`${prefixCls }-vertical`]: direction !== strings.DEFAULT_DIRECTION,
  187. [`${prefixCls }-large`]: size === 'large',
  188. });
  189. const progressTrackCls = cls({
  190. [`${prefixCls }-track`]: true,
  191. });
  192. const innerCls = cls(`${prefixCls }-track-inner`);
  193. const perc = this.calcPercent(percent);
  194. const percNumber = this.calcPercent(percentNumber);
  195. const innerStyle: Record<string, any> = {
  196. backgroundColor: stroke
  197. };
  198. if (direction === strings.DEFAULT_DIRECTION) {
  199. innerStyle.width = `${perc }%`;
  200. } else {
  201. innerStyle.height = `${perc }%`;
  202. }
  203. const text = format(percNumber);
  204. return (
  205. <div className={progressWrapperCls} style={style}>
  206. <div className={progressTrackCls} style={orbitStroke ? { backgroundColor: orbitStroke } : {}}>
  207. <div className={innerCls} style={innerStyle} />
  208. </div>
  209. {showInfo ? <div className={`${prefixCls }-line-text`}>{text}</div> : null}
  210. </div>
  211. );
  212. }
  213. render(): ReactNode {
  214. const { type } = this.props;
  215. if (type === 'line') {
  216. return this.renderLineProgress();
  217. } else {
  218. return this.renderCircleProgress();
  219. }
  220. }
  221. }
  222. export default Progress;