fileCard.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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, IconClear, IconFile, IconRefresh, IconEyeOpened } from '@douyinfe/semi-icons';
  7. import LocaleConsumer from '../locale/localeConsumer';
  8. import { Locale } from '../locale/interface';
  9. import Button from '../button/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 focusable={false} aria-hidden 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 focusable={false} aria-hidden 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 focusable={false} aria-hidden 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, renderPicPreviewIcon, 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 showPreview = status === strings.FILE_STATUS_SUCCESS && !this.props.showReplace;
  117. const filePicCardCls = cls({
  118. [`${prefixCls}-picture-file-card`]: true,
  119. [`${prefixCls}-picture-file-card-disabled`]: disabled,
  120. [`${prefixCls}-picture-file-card-show-pointer`]: typeof onPreviewClick !== 'undefined',
  121. [`${prefixCls}-picture-file-card-error`]: status === strings.FILE_STATUS_UPLOAD_FAIL,
  122. [`${prefixCls}-picture-file-card-uploading`]: showProgress
  123. });
  124. const retry = (
  125. <div role="button" tabIndex={0} className={`${prefixCls}-picture-file-card-retry`} onClick={e => this.onRetry(e)}>
  126. <IconRefresh className={`${prefixCls}-picture-file-card-icon-retry`} />
  127. </div>
  128. );
  129. const replace = (
  130. <Tooltip trigger="hover" position="top" content={locale.replace} showArrow={false} spacing={4}>
  131. <div role="button" tabIndex={0} className={`${prefixCls}-picture-file-card-replace`} onClick={(e): void => this.onReplace(e)}>
  132. <ReplaceSvg className={`${prefixCls}-picture-file-card-icon-replace`} />
  133. </div>
  134. </Tooltip>
  135. );
  136. const preview = (
  137. <div className={`${prefixCls}-picture-file-card-preview`}>
  138. {typeof renderPicPreviewIcon === 'function'? renderPicPreviewIcon(this.props): null}
  139. </div>
  140. );
  141. const close = (
  142. <div role="button" tabIndex={0} className={`${prefixCls}-picture-file-card-close`} onClick={e => this.onRemove(e)}>
  143. <IconClear className={`${prefixCls}-picture-file-card-icon-close`} />
  144. </div>
  145. );
  146. const picInfo = typeof renderPicInfo === 'function' ? renderPicInfo(this.props) : (
  147. <div className={`${prefixCls }-picture-file-card-pic-info`}>{index + 1}</div>
  148. );
  149. const thumbnail = typeof renderThumbnail === 'function' ? renderThumbnail(this.props) : <img src={url} alt={name} />;
  150. return (
  151. <div role="listitem" className={filePicCardCls} style={style} onClick={onPreviewClick}>
  152. {thumbnail}
  153. {showProgress ? <Progress percent={percent} type="circle" size="small" orbitStroke={'#FFF'} aria-label="uploading file progress" /> : null}
  154. {showRetry ? retry : null}
  155. {showReplace && replace}
  156. {showPreview && preview}
  157. {showPicInfo && picInfo}
  158. {!disabled && close}
  159. {this.renderPicValidateMsg()}
  160. </div>
  161. );
  162. }
  163. renderFile(locale: Locale["Upload"]) {
  164. const { name, size, percent, url, showRetry: propsShowRetry, showReplace: propsShowReplace, preview, previewFile, status, style, onPreviewClick, renderFileOperation } = this.props;
  165. const fileCardCls = cls({
  166. [`${prefixCls}-file-card`]: true,
  167. [`${prefixCls}-file-card-fail`]: status === strings.FILE_STATUS_VALID_FAIL || status === strings.FILE_STATUS_UPLOAD_FAIL,
  168. [`${prefixCls}-file-card-show-pointer`]: typeof onPreviewClick !== 'undefined',
  169. });
  170. const previewCls = cls({
  171. [`${prefixCls}-file-card-preview`]: true,
  172. [`${prefixCls}-file-card-preview-placeholder`]: !preview || previewFile
  173. });
  174. const infoCls = `${prefixCls}-file-card-info`;
  175. const closeCls = `${prefixCls}-file-card-close`;
  176. const replaceCls = `${prefixCls}-file-card-replace`;
  177. const showProgress = !(percent === 100 || typeof percent === 'undefined') && status === strings.FILE_STATUS_UPLOADING;
  178. // only show retry when upload fail & showRetry is true, no need to show during validate fail
  179. const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && propsShowRetry;
  180. const showReplace = status === strings.FILE_STATUS_SUCCESS && propsShowReplace;
  181. const fileSize = this.transSize(size);
  182. let previewContent: ReactNode = preview ? (<img src={url} alt={name} />) : (<IconFile size="large" />);
  183. if (previewFile) {
  184. previewContent = previewFile(this.props);
  185. }
  186. const operation = typeof renderFileOperation === 'function'? renderFileOperation(this.props) : <Button onClick={e => this.onRemove(e)} type="tertiary" icon={<IconClose />} theme="borderless" size="small" className={closeCls} />;
  187. return (
  188. <div role="listitem" className={fileCardCls} style={style} onClick={onPreviewClick}>
  189. <div className={previewCls}>
  190. {previewContent}
  191. </div>
  192. <div className={`${infoCls}-main`}>
  193. <div className={`${infoCls}-main-text`}>
  194. <span className={`${infoCls}-name`}>
  195. {name}
  196. </span>
  197. <span>
  198. <span className={`${infoCls}-size`}>{fileSize}</span>
  199. {showReplace && (
  200. <Tooltip trigger="hover" position="top" showArrow={false} content={locale.replace}>
  201. <Button
  202. onClick={e => this.onReplace(e)}
  203. type="tertiary"
  204. theme="borderless"
  205. size="small"
  206. icon={<DirectorySvg />}
  207. className={replaceCls}
  208. />
  209. </Tooltip>
  210. )}
  211. </span>
  212. </div>
  213. {showProgress ? (<Progress percent={percent} style={{ width: '100%' }} aria-label="uploading file progress" />) : null}
  214. <div className={`${infoCls}-main-control`}>
  215. <span className={`${infoCls}-validate-message`}>
  216. {this.renderValidateMessage()}
  217. </span>
  218. {showRetry ? <span role="button" tabIndex={0} className={`${infoCls}-retry`} onClick={e => this.onRetry(e)}>{locale.retry}</span> : null}
  219. </div>
  220. </div>
  221. {operation}
  222. </div>
  223. );
  224. }
  225. onRemove(e: MouseEvent): void {
  226. e.stopPropagation();
  227. this.props.onRemove();
  228. }
  229. onReplace(e: MouseEvent): void {
  230. e.stopPropagation();
  231. this.props.onReplace();
  232. }
  233. onRetry(e: MouseEvent): void {
  234. e.stopPropagation();
  235. this.props.onRetry();
  236. }
  237. render() {
  238. const { listType } = this.props;
  239. if (listType === strings.FILE_LIST_PIC) {
  240. return (
  241. <LocaleConsumer componentName="Upload">
  242. {(locale: Locale["Upload"]) => (this.renderPic(locale))}
  243. </LocaleConsumer>
  244. );
  245. }
  246. if (listType === strings.FILE_LIST_DEFAULT) {
  247. return (
  248. <LocaleConsumer componentName="Upload">
  249. {(locale: Locale["Upload"]) => (this.renderFile(locale))}
  250. </LocaleConsumer>
  251. );
  252. }
  253. return null;
  254. }
  255. }
  256. export default FileCard;