fileCard.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import React, { PureComponent, ReactNode, MouseEventHandler, MouseEvent, CSSProperties, SVGProps, FC } from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/upload/constants';
  5. import { getFileSize } from '@douyinfe/semi-foundation/upload/utils';
  6. import { IconAlertCircle, IconClose, IconFile, IconRefresh } from '@douyinfe/semi-icons';
  7. import LocaleConsumer from '../locale/localeConsumer';
  8. import { Locale } from '../locale/interface';
  9. import IconButton from '../iconButton/index';
  10. import Progress from '../progress/index';
  11. import Tooltip from '../tooltip/index';
  12. import Spin from '../spin/index';
  13. import { isElement } from '../_base/reactUtils';
  14. import { RenderFileItemProps } from './interface';
  15. const prefixCls = cssClasses.PREFIX;
  16. const ErrorSvg: FC<SVGProps<SVGSVGElement>> = (props = {}) => (
  17. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
  18. <circle cx="7.99992" cy="7.99992" r="6.66667" fill="white" />
  19. <path
  20. fillRule="evenodd"
  21. clipRule="evenodd"
  22. d="M15.3332 8.00008C15.3332 12.0502 12.0499 15.3334 7.99984 15.3334C3.94975 15.3334 0.666504 12.0502 0.666504 8.00008C0.666504 3.94999 3.94975 0.666748 7.99984 0.666748C12.0499 0.666748 15.3332 3.94999 15.3332 8.00008ZM8.99984 11.6667C8.99984 11.1145 8.55212 10.6667 7.99984 10.6667C7.44755 10.6667 6.99984 11.1145 6.99984 11.6667C6.99984 12.219 7.44755 12.6667 7.99984 12.6667C8.55212 12.6667 8.99984 12.219 8.99984 11.6667ZM7.99984 3.33341C7.27573 3.33341 6.7003 3.94171 6.74046 4.66469L6.94437 8.33495C6.97549 8.89513 7.4388 9.33341 7.99984 9.33341C8.56087 9.33341 9.02419 8.89513 9.05531 8.33495L9.25921 4.66469C9.29938 3.94171 8.72394 3.33341 7.99984 3.33341Z" fill="#F93920"
  23. />
  24. </svg>
  25. );
  26. const ReplaceSvg: FC<SVGProps<SVGSVGElement>> = (props = {}) => (
  27. <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
  28. <circle cx="14" cy="14" r="14" fill="#16161A" fillOpacity="0.6" />
  29. <path d="M9 10.25V18.25L10.25 13.25H17.875V11.75C17.875 11.4739 17.6511 11.25 17.375 11.25H14L12.75 9.75H9.5C9.22386 9.75 9 9.97386 9 10.25Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
  30. <path d="M18 18.25L19 13.25H10.2031L9 18.25H18Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
  31. </svg>
  32. );
  33. const DirectorySvg: FC<SVGProps<SVGSVGElement>> = (props = {}) => (
  34. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
  35. <path d="M6 17V7.58824C6 7.26336 6.26863 7 6.6 7H10.5L12 8.76471H16.05C16.3814 8.76471 16.65 9.02806 16.65 9.35294V11.1176H7.5L6 17ZM6 17L7.44375 11.1176H18L16.8 17L6 17Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
  36. </svg>
  37. );
  38. export interface FileCardProps extends RenderFileItemProps {
  39. className?: string;
  40. style?: CSSProperties;
  41. }
  42. class FileCard extends PureComponent<FileCardProps> {
  43. static propTypes = {
  44. className: PropTypes.string,
  45. disabled: PropTypes.bool,
  46. listType: PropTypes.string,
  47. name: PropTypes.string,
  48. onPreviewClick: PropTypes.func,
  49. onRemove: PropTypes.func,
  50. onReplace: PropTypes.func,
  51. onRetry: PropTypes.func,
  52. percent: PropTypes.number,
  53. preview: PropTypes.bool,
  54. previewFile: PropTypes.func,
  55. showReplace: PropTypes.bool,
  56. showRetry: PropTypes.bool,
  57. size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  58. status: PropTypes.string,
  59. style: PropTypes.object,
  60. url: PropTypes.string,
  61. validateMessage: PropTypes.node,
  62. index: PropTypes.number
  63. };
  64. static defaultProps = {
  65. listType: strings.FILE_LIST_DEFAULT,
  66. name: '',
  67. onRemove: (): void => undefined,
  68. onRetry: (): void => undefined,
  69. preview: false,
  70. size: '',
  71. };
  72. transSize(size: string | number): string {
  73. if (typeof size === 'number') {
  74. return getFileSize(size);
  75. }
  76. return size;
  77. }
  78. renderValidateMessage(): ReactNode {
  79. const { status, validateMessage } = this.props;
  80. let content = null;
  81. switch (true) {
  82. case typeof validateMessage === 'string' && status === strings.FILE_STATUS_VALIDATING:
  83. content = (<><Spin size="small" wrapperClassName={`${prefixCls}-file-card-icon-loading`} />{validateMessage}</>);
  84. break;
  85. case typeof validateMessage === 'string':
  86. content = (<><IconAlertCircle className={`${prefixCls}-file-card-icon-error`} />{validateMessage}</>);
  87. break;
  88. case isElement(validateMessage):
  89. content = validateMessage;
  90. break;
  91. default:
  92. break;
  93. }
  94. return content;
  95. }
  96. renderPicValidateMsg(): ReactNode {
  97. const { status, validateMessage } = this.props;
  98. let icon = null;
  99. switch (true) {
  100. case validateMessage && status === strings.FILE_STATUS_VALIDATING:
  101. icon = (<Spin size="small" wrapperClassName={`${prefixCls}-picture-file-card-icon-loading`} />);
  102. break;
  103. case validateMessage && (status === strings.FILE_STATUS_VALID_FAIL || status === strings.FILE_STATUS_UPLOAD_FAIL):
  104. icon = (<div className={`${prefixCls}-picture-file-card-icon-error`}><ErrorSvg /></div>);
  105. break;
  106. default:
  107. break;
  108. }
  109. return icon ? <Tooltip content={validateMessage} trigger="hover" position="bottom">{icon}</Tooltip> : null;
  110. }
  111. renderPic(locale: Locale['Upload']): ReactNode {
  112. const { url, percent, status, disabled, style, onPreviewClick, showPicInfo, renderPicInfo, renderThumbnail, name, index } = this.props;
  113. const showProgress = status === strings.FILE_STATUS_UPLOADING && percent !== 100;
  114. const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && this.props.showRetry;
  115. const showReplace = status === strings.FILE_STATUS_SUCCESS && this.props.showReplace;
  116. const filePicCardCls = cls({
  117. [`${prefixCls}-picture-file-card`]: true,
  118. [`${prefixCls}-picture-file-card-disabled`]: disabled,
  119. [`${prefixCls}-picture-file-card-show-pointer`]: typeof onPreviewClick !== 'undefined',
  120. [`${prefixCls}-picture-file-card-error`]: status === strings.FILE_STATUS_UPLOAD_FAIL,
  121. [`${prefixCls}-picture-file-card-uploading`]: showProgress
  122. });
  123. const closeCls = `${prefixCls}-picture-file-card-close`;
  124. const retry = (
  125. <div
  126. className={`${prefixCls}-picture-file-card-retry`} onClick={e => this.onRetry(e)}>
  127. <IconRefresh className={`${prefixCls}-picture-file-card-icon-retry`} />
  128. </div>
  129. );
  130. const replace = (
  131. <Tooltip trigger="hover" position="top" content={locale.replace} showArrow={false} spacing={4}>
  132. <div
  133. className={`${prefixCls}-picture-file-card-replace`} onClick={(e): void => this.onReplace(e)}>
  134. <ReplaceSvg className={`${prefixCls}-picture-file-card-icon-replace`} />
  135. </div>
  136. </Tooltip>
  137. );
  138. const picInfo = typeof renderPicInfo === 'function' ? renderPicInfo(this.props) : (
  139. <div className={`${prefixCls }-picture-file-card-pic-info`}>{index + 1}</div>
  140. );
  141. const thumbnail = typeof renderThumbnail === 'function' ? renderThumbnail(this.props) : <img src={url} alt={`picture of ${name}`} />;
  142. return (
  143. <div className={filePicCardCls} style={style} onClick={onPreviewClick}>
  144. {thumbnail}
  145. {showProgress ? <Progress percent={percent} type="circle" size="small" orbitStroke={'#FFF'} /> : null}
  146. {showRetry ? retry : null}
  147. {showReplace && replace}
  148. {showPicInfo && picInfo}
  149. {!disabled && (
  150. <div className={closeCls}>
  151. <IconClose size="extra-small" onClick={e => this.onRemove(e)} />
  152. </div>
  153. )}
  154. {this.renderPicValidateMsg()}
  155. </div>
  156. );
  157. }
  158. renderFile(locale: Locale["Upload"]) {
  159. const { name, size, percent, url, showRetry: propsShowRetry, showReplace: propsShowReplace, preview, previewFile, status, style, onPreviewClick } = this.props;
  160. const fileCardCls = cls({
  161. [`${prefixCls}-file-card`]: true,
  162. [`${prefixCls}-file-card-fail`]: status === strings.FILE_STATUS_VALID_FAIL || status === strings.FILE_STATUS_UPLOAD_FAIL,
  163. [`${prefixCls}-file-card-show-pointer`]: typeof onPreviewClick !== 'undefined',
  164. });
  165. const previewCls = cls({
  166. [`${prefixCls}-file-card-preview`]: true,
  167. [`${prefixCls}-file-card-preview-placeholder`]: !preview || previewFile
  168. });
  169. const infoCls = `${prefixCls}-file-card-info`;
  170. const closeCls = `${prefixCls}-file-card-close`;
  171. const replaceCls = `${prefixCls}-file-card-replace`;
  172. const showProgress = !(percent === 100 || typeof percent === 'undefined') && status === strings.FILE_STATUS_UPLOADING;
  173. // only show retry when upload fail & showRetry is true, no need to show during validate fail
  174. const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && propsShowRetry;
  175. const showReplace = status === strings.FILE_STATUS_SUCCESS && propsShowReplace;
  176. const fileSize = this.transSize(size);
  177. let previewContent: ReactNode = preview ? (<img src={url} />) : (<IconFile size="large" />);
  178. if (previewFile) {
  179. previewContent = previewFile(this.props);
  180. }
  181. return (
  182. <div className={fileCardCls} style={style} onClick={onPreviewClick}>
  183. <div className={previewCls}>
  184. {previewContent}
  185. </div>
  186. <div className={`${infoCls}-main`}>
  187. <div className={`${infoCls}-main-text`}>
  188. <span className={`${infoCls}-name`}>
  189. {name}
  190. </span>
  191. <span>
  192. <span className={`${infoCls}-size`}>{fileSize}</span>
  193. {showReplace && (
  194. <Tooltip trigger="hover" position="top" showArrow={false} content={locale.replace}>
  195. <IconButton
  196. onClick={e => this.onReplace(e)}
  197. type="tertiary"
  198. theme="borderless"
  199. size="small"
  200. icon={<DirectorySvg />}
  201. className={replaceCls}
  202. />
  203. </Tooltip>
  204. )}
  205. </span>
  206. </div>
  207. {showProgress ? (<Progress percent={percent} style={{ width: '100%' }} />) : null}
  208. <div className={`${infoCls}-main-control`}>
  209. <span className={`${infoCls}-validate-message`}>
  210. {this.renderValidateMessage()}
  211. </span>
  212. {showRetry ? <span className={`${infoCls}-retry`} onClick={e => this.onRetry(e)}>{locale.retry}</span> : null}
  213. </div>
  214. </div>
  215. <IconButton
  216. onClick={e => this.onRemove(e)}
  217. type="tertiary"
  218. icon={<IconClose />}
  219. theme="borderless"
  220. size="small"
  221. className={closeCls}
  222. />
  223. </div>
  224. );
  225. }
  226. onRemove(e: MouseEvent): void {
  227. e.stopPropagation();
  228. this.props.onRemove(this.props, e);
  229. }
  230. onReplace(e: MouseEvent): void {
  231. e.stopPropagation();
  232. this.props.onReplace(this.props, e);
  233. }
  234. onRetry(e: MouseEvent): void {
  235. e.stopPropagation();
  236. this.props.onRetry(this.props, e);
  237. }
  238. render() {
  239. const { listType } = this.props;
  240. if (listType === strings.FILE_LIST_PIC) {
  241. return (
  242. <LocaleConsumer componentName="Upload">
  243. {(locale: Locale["Upload"]) => (this.renderPic(locale))}
  244. </LocaleConsumer>
  245. );
  246. }
  247. if (listType === strings.FILE_LIST_DEFAULT) {
  248. return (
  249. <LocaleConsumer componentName="Upload">
  250. {(locale: Locale["Upload"]) => (this.renderFile(locale))}
  251. </LocaleConsumer>
  252. );
  253. }
  254. return null;
  255. }
  256. }
  257. export default FileCard;