import React, { ReactElement, ReactNode } from 'react'; import { action as mobxAction, computed, IReactionDisposer, observable, reaction, runInAction, toJS } from 'mobx'; import { observer } from 'mobx-react'; import classNames from 'classnames'; import omit from 'lodash/omit'; import noop from 'lodash/noop'; import isNil from 'lodash/isNil'; import isString from 'lodash/isString'; import isFunction from 'lodash/isFunction'; import { getConfig, Uploader } from 'choerodon-ui/dataset'; import { AttachmentValue, AttachmentFileProps } from 'choerodon-ui/dataset/configure'; import { UploaderProps } from 'choerodon-ui/dataset/uploader/Uploader'; import { DownloadAllMode } from 'choerodon-ui/dataset/data-set/enum'; import { AttachmentConfig } from 'choerodon-ui/lib/configure'; import { Size } from 'choerodon-ui/lib/_util/enum'; import Trigger from 'choerodon-ui/lib/trigger/Trigger'; import RcUpload from 'choerodon-ui/lib/rc-components/upload'; import { Action } from 'choerodon-ui/lib/trigger/enum'; import { pxToRem } from 'choerodon-ui/lib/_util/UnitConvertor'; import Button, { ButtonProps } from '../button/Button'; import { $l } from '../locale-context'; import { ButtonColor, FuncType } from '../button/enum'; import AttachmentList from './AttachmentList'; import AttachmentGroup from './AttachmentGroup'; import { FormField, FormFieldProps } from '../field/FormField'; import autobind from '../_util/autobind'; import Modal from '../modal'; import AttachmentFile, { FileLike } from '../data-set/AttachmentFile'; import { sortAttachments } from './utils'; import ObserverSelect from '../select/Select'; import BUILT_IN_PLACEMENTS from '../trigger-field/placements'; import attachmentStore from '../stores/AttachmentStore'; import { FieldType } from '../data-set/enum'; import { ValidationMessages } from '../validator/Validator'; import ValidationResult from '../validator/ValidationResult'; import { open } from '../modal-container/ModalContainer'; import Icon from '../icon'; import Dragger from './Dragger'; import { ShowHelp } from '../field/enum'; import { FIELD_SUFFIX } from '../form/utils'; import { showValidationMessage } from '../field/utils'; import { ShowValidation } from '../form/enum'; import { getIf } from '../data-set/utils'; import { ATTACHMENT_TARGET } from './Item'; import TemplateDownloadButton from './TemplateDownloadButton'; import { hide, show } from '../tooltip/singleton'; export type AttachmentListType = 'text' | 'picture' | 'picture-card'; export enum AttachmentButtonType { download = 'download', remove = 'remove', history = 'history', } export type AttachmentButtons = | AttachmentButtonType | [AttachmentButtonType, ButtonProps] | ReactElement<ButtonProps>; export interface AttachmentProps extends FormFieldProps, ButtonProps, UploaderProps { listType?: AttachmentListType; viewMode?: 'none' | 'list' | 'popup'; sortable?: boolean; pictureWidth?: number; count?: number; max?: number; listLimit?: number; showHistory?: boolean; showSize?: boolean; showValidation?: ShowValidation; attachments?: (AttachmentFile | FileLike)[]; onAttachmentsChange?: (attachments: AttachmentFile[]) => void; onRemove?: (attachment: AttachmentFile) => boolean | void; getUUID?: () => Promise<string> | string; downloadAll?: ButtonProps | boolean; previewTarget?: string; dragUpload?: boolean; dragBoxRender?: ReactNode[]; template?: AttachmentValue; buttons?: AttachmentButtons[]; __inGroup?: boolean; getPreviewUrl?: (props: AttachmentFileProps) => (string | (() => string | Promise<string>) | undefined); } export type Sort = { type: 'time' | 'name'; order: 'asc' | 'desc'; custom?: boolean; }; const defaultSort: Sort = { type: 'time', order: 'asc', custom: true, }; @observer export default class Attachment extends FormField<AttachmentProps> { static displayName = 'Attachment'; static Dragger: typeof Dragger; static defaultProps = { ...FormField.defaultProps, suffixCls: 'attachment', multiple: true, sortable: true, showSize: true, downloadAll: true, listType: 'text', viewMode: 'list', dragUpload: false, }; // eslint-disable-next-line camelcase static __IS_IN_CELL_EDITOR = true; // eslint-disable-next-line camelcase static __PRO_ATTACHMENT = true; static Group = AttachmentGroup; @observable sort?: Sort; @observable popup?: boolean; @observable dragState?: string; uploader?: Uploader; @observable tempAttachmentUUID?: string | undefined; get help() { return this.getDisplayProp('help'); } @computed get showAttachmentHelp() { const defaultShowHelp = this.getContextConfig('showHelp'); const { viewMode } = this.props; const { showHelp } = this; const { formNode } = this.context; if (showHelp === ShowHelp.none || (formNode && showHelp === ShowHelp.label) || (viewMode === 'popup' && (showHelp || defaultShowHelp) === ShowHelp.label && formNode)) { return ShowHelp.none; } if (viewMode === 'popup') { return ShowHelp.tooltip; } return ShowHelp.newLine; } get bucketName() { return this.getProp('bucketName'); } get bucketDirectory() { return this.getProp('bucketDirectory'); } get storageCode() { return this.getProp('storageCode'); } get fileKey() { return this.getProp('fileKey') || this.getContextConfig('attachment').defaultFileKey; } get isPublic() { return this.getProp('isPublic'); } get attachments(): AttachmentFile[] | undefined { const { field } = this; if (field) { return field.getAttachments(this.record, this.tempAttachmentUUID); } if (this.getValue()) { return this.observableProps.attachments; } } set attachments(attachments: AttachmentFile[] | undefined) { runInAction(() => { const { field } = this; if (field) { field.setAttachments(attachments, this.record, this.tempAttachmentUUID); } else { this.observableProps.attachments = attachments; } if (attachments) { const { onAttachmentsChange } = this.props; if (onAttachmentsChange) { onAttachmentsChange(attachments); } } }); } get count(): number | undefined { const { attachments, field } = this; if (attachments) { return attachments.length; } if (field) { const attachmentCount = field.getAttachmentCount(this.record); if (attachmentCount !== undefined) { return attachmentCount; } } const { count } = this.observableProps; return count; } get defaultValidationMessages(): ValidationMessages { const label = this.getProp('label'); return { valueMissing: $l('Attachment', label ? 'value_missing' : 'value_missing_no_label', { label }), }; } get accept(): string[] | undefined { return this.getProp('accept'); } private reaction?: IReactionDisposer; componentDidMount() { super.componentDidMount(); this.fetchCount(); this.reaction = reaction(() => this.record, () => this.fetchCount()); } componentDidUpdate(prevProps: AttachmentProps) { const { value, viewMode } = this.props; if (prevProps.value !== value || prevProps.viewMode !== viewMode) { this.fetchCount(); } } componentWillUnmount() { super.componentWillUnmount(); const { reaction } = this; if (reaction) { reaction(); delete this.reaction; } } getFieldType(): FieldType { return FieldType.attachment; } getObservableProps(props, context): any { const { count, attachments } = props; const { observableProps = { count, attachments } } = this; return { ...super.getObservableProps(props, context), count: count === undefined ? observableProps.count : count, attachments: attachments ? attachments.map(attachment => { if (attachment instanceof AttachmentFile) { return attachment; } return new AttachmentFile(attachment); }) : observableProps.attachments, }; } getValidAttachments(): AttachmentFile[] | undefined { const { attachments } = this; if (attachments) { return attachments.filter(({ status }) => !status || ['success', 'done'].includes(status)); } } getValidatorProp(key: string) { if (key === 'attachmentCount') { const attachments = this.getValidAttachments(); const count = attachments ? attachments.length : this.count; return count || 0; } return super.getValidatorProp(key); } fetchCount() { const { field } = this; const { viewMode } = this.props; if (viewMode !== 'list' && isNil(this.count)) { const value = this.getValue(); if (value) { const { isPublic } = this; if (field) { field.fetchAttachmentCount(value, isPublic, this.record); } else { const { batchFetchCount } = this.getContextConfig('attachment'); if (batchFetchCount && !this.attachments) { const { bucketName, bucketDirectory, storageCode } = this; attachmentStore.fetchCountInBatch({ attachmentUUID: value, bucketName, bucketDirectory, storageCode, isPublic, }).then(mobxAction((count) => { this.observableProps.count = count || 0; })); } } } } } @autobind updateCacheCount() { const attachmentUUID = this.getValue(); if (attachmentUUID && this.count) { attachmentStore.updateCacheCount(attachmentUUID, this.count); } } @autobind handleDataSetLoad() { this.fetchCount(); } getOmitPropsKeys(): string[] { return super.getOmitPropsKeys().concat([ 'value', 'accept', 'action', 'data', 'headers', 'buttons', 'withCredentials', 'sortable', 'listType', 'viewMode', 'fileKey', 'fileSize', 'useChunk', 'chunkSize', 'chunkThreads', 'bucketName', 'bucketDirectory', 'storageCode', 'count', 'max', 'listLimit', 'dragBoxRender', 'dragUpload', 'showHistory', 'showSize', 'isPublic', 'downloadAll', 'attachments', 'onAttachmentsChange', 'beforeUpload', 'onUploadProgress', 'onUploadSuccess', 'onUploadError', 'onRemove', 'getPreviewUrl', ]); } isAcceptFile(attachment: AttachmentFile, accept: string[]): boolean { const acceptTypes = accept.map(type => ( new RegExp(type.replace(/\./g, '\\.').replace(/\*/g, '.*')) )); const { name, type } = attachment; return acceptTypes.some(acceptType => acceptType.test(name) || acceptType.test(type)); } async getAttachmentUUID(): Promise<string> { this.autoCreate(); const oldAttachmentUUID = this.tempAttachmentUUID || this.getValue(); const attachmentUUID = oldAttachmentUUID || (await this.fetchAttachmentUUID()); if (attachmentUUID !== oldAttachmentUUID) { runInAction(() => { this.tempAttachmentUUID = attachmentUUID; }); } return attachmentUUID; } fetchAttachmentUUID(): Promise<string> | string { const { getUUID = this.getContextConfig('attachment').getAttachmentUUID } = this.props; if (!getUUID) { throw new Error('no getAttachmentUUID hook in global configure.'); } return getUUID({ isPublic: this.isPublic }); } @mobxAction async uploadAttachments(attachments: AttachmentFile[]): Promise<void> { const max = this.getProp('max'); if (max > 0 && (this.count || 0) + attachments.length > max) { Modal.error($l('Attachment', 'file_list_max_length', { count: max })); return; } const oldAttachments = this.attachments || []; if (this.multiple) { this.attachments = [...oldAttachments.slice(), ...attachments]; } else { oldAttachments.forEach(attachment => this.doRemove(attachment)); this.attachments = [...attachments]; } try { await Promise.all(attachments.map((attachment) => this.upload(attachment))); } finally { this.changeOrder(); } } @autobind async uploadAttachment(attachment: AttachmentFile) { await this.upload(attachment); if (attachment.status === 'success') { this.changeOrder(); } } getUploaderProps(): UploaderProps { const { bucketName, bucketDirectory, storageCode, isPublic, fileKey, accept } = this; const fileSize = this.getProp('fileSize'); const chunkSize = this.getProp('chunkSize'); const chunkThreads = this.getProp('chunkThreads'); const useChunk = this.getProp('useChunk'); const { action, data, headers, withCredentials, beforeUpload, onUploadProgress, onUploadSuccess, onUploadError, } = this.props; return { accept, action, data, headers, fileKey, withCredentials, bucketName, bucketDirectory, storageCode, isPublic, fileSize, chunkSize, chunkThreads, useChunk, beforeUpload, onUploadProgress, onUploadSuccess, onUploadError, }; } async upload(attachment: AttachmentFile) { try { const uploader = getIf(this, 'uploader', () => { return new Uploader( {}, { getConfig: this.getContextConfig as typeof getConfig, }, ); }); uploader.setProps(this.getUploaderProps()); const result = await uploader.upload(attachment, this.attachments || [attachment], this.tempAttachmentUUID); if (result === false) { this.removeAttachment(attachment); } else { runInAction(() => { const { tempAttachmentUUID } = this; if (attachment.status === 'success') { this.updateCacheCount(); if (tempAttachmentUUID) { this.tempAttachmentUUID = undefined; this.setValue(tempAttachmentUUID); } } else { this.checkValidity(); } }); } } catch (e) { this.removeAttachment(attachment); throw e; } } getOtherProps(): any { const otherProps = super.getOtherProps(); otherProps.onClick = this.handleClick; return otherProps; } processFiles(files: File[], attachmentUUID: string): AttachmentFile[] { return files.map((file, index: number) => new AttachmentFile({ uid: this.getUid(index), url: URL.createObjectURL(file), name: file.name, size: file.size, type: file.type, lastModified: file.lastModified, originFileObj: file, creationDate: new Date(), attachmentUUID, })); } @autobind @mobxAction handleChange(e) { const { target } = e; if (target.value) { const files: File[] = [...target.files]; target.value = ''; this.getAttachmentUUID().then((uuid) => { this.uploadAttachments(this.processFiles(files, uuid)); }); } } doRemove(attachment: AttachmentFile): Promise<any> | undefined { const { onRemove: onAttachmentRemove = noop } = this.props; return Promise.resolve(onAttachmentRemove(attachment)).then(mobxAction(ret => { if (ret !== false) { const { onRemove } = this.getContextConfig('attachment'); if (onRemove) { if (attachment.status === 'error' || attachment.invalid) { return this.removeAttachment(attachment); } const attachmentUUID = this.getValue(); if (attachmentUUID) { const { bucketName, bucketDirectory, storageCode, isPublic, multiple } = this; attachment.status = 'deleting'; return onRemove({ attachment, attachmentUUID, bucketName, bucketDirectory, storageCode, isPublic }, multiple) .then(mobxAction((result) => { if (result !== false) { this.removeAttachment(attachment); } attachment.status = 'done'; })) .catch(mobxAction(() => { attachment.status = 'done'; })); } } } })); } @autobind handleHistory(attachment: AttachmentFile, attachmentUUID: string) { const { renderHistory } = this.getContextConfig('attachment') as AttachmentConfig; if (renderHistory) { const { bucketName, bucketDirectory, storageCode } = this; open({ title: $l('Attachment', 'operation_records'), children: renderHistory({ attachment, attachmentUUID, bucketName, bucketDirectory, storageCode, }), cancelButton: false, okText: $l('Modal', 'close'), drawer: true, }); } } @autobind handleRemove(attachment: AttachmentFile): Promise<any> | undefined { return this.doRemove(attachment); } @autobind @mobxAction handleAttachmentsChange(attachments: AttachmentFile[] | undefined) { this.observableProps.attachments = attachments; } @autobind handleFetchAttachment(fetchProps: { bucketName?: string; bucketDirectory?: string; storageCode?: string; attachmentUUID: string; isPublic?: boolean; }) { const { field } = this; if (field) { field.fetchAttachments(fetchProps, this.record); } else { const { fetchList } = this.getContextConfig('attachment'); if (fetchList) { fetchList(fetchProps).then((results) => { this.attachments = results.map(file => new AttachmentFile(file)); }); } } } @autobind handlePreview() { this.setPopup(false); } @mobxAction removeAttachment(attachment: AttachmentFile): undefined { const { attachments } = this; if (attachments) { const index = attachments.indexOf(attachment); if (index !== -1) { attachments.splice(index, 1); this.attachments = attachments; this.checkValidity(); this.updateCacheCount(); } } return undefined; } handleDragUpload = (file: File, files: File[]) => { if (files.indexOf(file) === files.length - 1) { this.getAttachmentUUID().then((uuid) => { this.uploadAttachments(this.processFiles(files, uuid)); }); } return false; }; @autobind handleClick(e) { const { element } = this; if (element) { element.click(); } const { onClick } = this.props; if (onClick) { onClick(e); } } getUid(index: number): string { const { prefixCls } = this; return `${prefixCls}-${Date.now()}-${index}`; } renderHeaderLabel() { const { viewMode } = this.props; if (this.hasFloatLabel || viewMode === 'popup') { const label = this.getLabel(); if (label) { const { prefixCls } = this; return ( <span className={classNames(`${prefixCls}-header-label`, { [`${prefixCls}-required`]: this.getProp('required') })}> {label} </span> ); } } } isDisabled(): boolean { if (super.isDisabled()) { return true; } const max = this.getProp('max'); if (max) { const { count = 0 } = this; return count >= max; } return false; } isValid(): boolean { const { attachments } = this; if (attachments && attachments.some(({ status, invalid }) => invalid || status === 'error')) { return false; } return super.isValid(); } renderTemplateDownloadButton(): ReactElement<ButtonProps> | undefined { if (!this.readOnly) { const template = this.getProp('template'); if (template) { const { bucketName, bucketDirectory, storageCode, isPublic, attachmentUUID } = template; if (attachmentUUID) { const { previewTarget = ATTACHMENT_TARGET, viewMode, color, funcType = viewMode === 'popup' ? FuncType.flat : FuncType.link } = this.props; return ( <TemplateDownloadButton attachmentUUID={attachmentUUID} bucketName={bucketName} bucketDirectory={bucketDirectory} storageCode={storageCode} isPublic={isPublic} target={previewTarget} funcType={funcType} color={color} /> ); } } } } renderUploadBtn(isCardButton: boolean, label?: ReactNode): ReactElement<ButtonProps> { const { count = 0, multiple, prefixCls, accept, props: { children, viewMode, }, } = this; const buttonProps = this.getOtherProps(); const { ref, style, name, onChange, ...rest } = buttonProps; const max = this.getProp('max'); const uploadProps = { multiple, accept: accept ? accept.join(',') : undefined, name: name || this.fileKey, type: 'file', ref, onChange, }; const width = isCardButton ? pxToRem(this.getPictureWidth()) : undefined; const countText = multiple && (max ? `${count}/${max}` : count) || undefined; return isCardButton ? ( <Button funcType={FuncType.link} key="upload-btn" icon="add" {...rest} className={classNames(`${prefixCls}-card-button`, this.getClassName())} style={{ ...style, width, height: width }} > <div>{children || $l('Attachment', 'upload_picture')}</div> {countText ? <div>{countText}</div> : undefined} <input key="upload" {...uploadProps} style={{ width: 0, height: 0, display: 'block' }} /> </Button> ) : ( <Button funcType={viewMode === 'popup' ? FuncType.flat : FuncType.link} key="upload-btn" icon="file_upload" color={this.valid ? ButtonColor.primary : ButtonColor.red} {...rest} className={viewMode === 'popup' ? this.getMergedClassNames() : this.getClassName()} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} > {children || $l('Attachment', 'upload_attachment')}{label && <>({label})</>} {countText} <input key="upload" {...uploadProps} style={{ width: 0, height: 0, display: 'block' }} /> </Button> ); } showTooltip(e): boolean { if (this.showValidation === ShowValidation.tooltip) { const message = this.getTooltipValidationMessage(); if (message) { showValidationMessage(e, message, this.context.getTooltipTheme('validation'), this.context.getTooltipPlacement('validation'), this.getContextConfig); return true; } } return false; } renderViewButton(label?: ReactNode): ReactElement<ButtonProps> { const { children, multiple, viewMode } = this.props; const rest = this.getOtherProps(); return ( <Button funcType={viewMode === 'popup' ? FuncType.flat : FuncType.link} key="view-btn" icon="attach_file" color={ButtonColor.primary} {...omit(rest, ['ref'])} className={this.getMergedClassNames()} > {children || $l('Attachment', 'view_attachment')}{label && <>({label})</>} {multiple && this.count || undefined} </Button> ); } @mobxAction handleSort(sort: Sort) { this.sort = sort; } @autobind @mobxAction handleOrderChange(props) { const { attachments } = props; this.attachments = attachments; this.changeOrder(); } @mobxAction changeOrder() { this.sort = { ...defaultSort, ...this.sort, custom: true, }; const { sortable } = this.props; if (sortable) { const { onOrderChange } = this.getContextConfig('attachment'); if (onOrderChange) { const attachmentUUID = this.getValue(); if (attachmentUUID) { const attachments = this.getValidAttachments(); if (attachments) { const { bucketName, bucketDirectory, storageCode, isPublic } = this; onOrderChange({ bucketName, bucketDirectory, storageCode, attachments, attachmentUUID, isPublic, }); } } } } } @autobind getSortSelectPopupContainer() { return this.wrapper; } renderSorter(): ReactNode { const { sortable, viewMode } = this.props; if (sortable) { const { prefixCls, attachments } = this; if (attachments && attachments.length > 1) { const { type, order } = this.sort || defaultSort; return ( <> <ObserverSelect value={type} onChange={(newType) => this.handleSort({ type: newType, order, custom: false })} clearButton={false} isFlat popupPlacement="bottomRight" getPopupContainer={viewMode === 'popup' ? this.getSortSelectPopupContainer : undefined} size={Size.small} border={false} > <ObserverSelect.Option value="time">{$l('Attachment', 'by_upload_time')}</ObserverSelect.Option> <ObserverSelect.Option value="name">{$l('Attachment', 'by_name')}</ObserverSelect.Option> </ObserverSelect> <Button funcType={FuncType.link} className={classNames(`${prefixCls}-order-icon`, order)} onClick={() => this.handleSort({ type, order: order === 'asc' ? 'desc' : 'asc', custom: false })} /> </> ); } } } renderUploadList(uploadButton?: ReactNode) { const { listType, sortable, listLimit, showHistory, showSize, previewTarget, buttons, getPreviewUrl, disabled, } = this.props; let mergeButtons:AttachmentButtons[] = [AttachmentButtonType.download, AttachmentButtonType.remove]; if (buttons) { mergeButtons = [...mergeButtons, ...buttons]; } if (showHistory) { mergeButtons.unshift(AttachmentButtonType.history); } const { attachments } = this; const attachmentUUID = this.tempAttachmentUUID || this.getValue(); if (attachmentUUID || uploadButton || (attachments && attachments.length)) { const { bucketName, bucketDirectory, storageCode, readOnly, isPublic } = this; const width = this.getPictureWidth(); return ( <AttachmentList prefixCls={`${this.prefixCls}-list`} pictureWidth={width} listType={listType} attachments={sortAttachments(attachments, this.sort || defaultSort)} bucketName={bucketName} bucketDirectory={bucketDirectory} storageCode={storageCode} attachmentUUID={attachmentUUID} uploadButton={uploadButton} sortable={sortable} showSize={showSize} readOnly={readOnly} disabled={disabled} isPublic={isPublic} limit={readOnly ? listLimit : undefined} previewTarget={previewTarget} onHistory={showHistory ? this.handleHistory : undefined} onRemove={this.handleRemove} onUpload={this.uploadAttachment} onOrderChange={this.handleOrderChange} onFetchAttachments={this.handleFetchAttachment} onAttachmentsChange={this.handleAttachmentsChange} onPreview={this.handlePreview} record={this.record} buttons={mergeButtons} getPreviewUrl={getPreviewUrl} /> ); } } renderHeader(uploadBtn?: ReactNode) { const { prefixCls, count, props: { downloadAll, viewMode, __inGroup } } = this; const label = (!__inGroup || count) && this.renderHeaderLabel(); const buttons: ReactNode[] = []; if (uploadBtn) { buttons.push( uploadBtn, this.renderTemplateDownloadButton(), ); } const { downloadAllMode = DownloadAllMode.readOnly } = this.getContextConfig('attachment'); if ((downloadAllMode === DownloadAllMode.readOnly && this.readOnly) || (downloadAllMode === DownloadAllMode.always)) { if (this.count) { if (downloadAll) { const { getDownloadAllUrl } = this.getContextConfig('attachment'); if (getDownloadAllUrl) { const attachmentUUID = this.getValue(); if (attachmentUUID) { const { bucketName, bucketDirectory, storageCode, isPublic } = this; const downloadAllUrl = getDownloadAllUrl({ bucketName, bucketDirectory, storageCode, attachmentUUID, isPublic, }); if (downloadAllUrl) { const downProps: ButtonProps = { key: 'download', icon: 'get_app', funcType: FuncType.link, href: isString(downloadAllUrl) ? downloadAllUrl : undefined, onClick: isFunction(downloadAllUrl) ? downloadAllUrl : undefined, target: '_blank', color: ButtonColor.primary, children: $l('Attachment', 'download_all'), }; buttons.push(<Button {...downProps} {...downloadAll} />); } } } } } else if (viewMode !== 'popup' && !__inGroup) { const viewProps: ButtonProps = { key: 'view', funcType: FuncType.link, disabled: true, children: $l('Attachment', 'no_attachments'), }; buttons.push( <Button {...viewProps} />, ); } } const hasButton = buttons.length; const sorter = this.renderSorter(); const divider = !__inGroup && label && hasButton ? <span key="divider" className={`${prefixCls}-header-divider`} /> : undefined; if (label || divider || hasButton || sorter) { return ( <div className={`${prefixCls}-header`}> {label} {divider} <div className={`${prefixCls}-header-buttons`}> {buttons} </div> {sorter} </div> ); } } @autobind renderWrapper(): ReactNode { const { prefixCls } = this; return ( <div className={`${prefixCls}-popup-inner`} ref={this.wrapperReference}> {this.renderWrapperList()} </div> ); } @autobind getTooltipValidationMessage(): ReactNode { const validationMessage = this.renderValidationResult(); if (!this.isValidationMessageHidden(validationMessage)) { return validationMessage; } } renderValidationResult(validationResult?: ValidationResult): ReactNode { const message = super.renderValidationResult(validationResult); if (message) { return ( <div className={`${this.prefixCls}-validation-message`}> {message} </div> ); } } renderEmpty() { const { viewMode } = this.props; if (viewMode === 'popup' && !this.count) { return ( <div className={`${this.prefixCls}-empty`}> {this.getContextConfig('renderEmpty')('Attachment')} </div> ); } } getWrapperClassNames() { const { prefixCls, props: { className, size }, } = this; return classNames( `${prefixCls}-wrapper`, className, { [`${prefixCls}-sm`]: size === Size.small, [`${prefixCls}-lg`]: size === Size.large, }, ); } renderWrapperList(uploadBtn?: ReactNode) { const { prefixCls, props: { viewMode, listType, __inGroup } } = this; const isCard = listType === 'picture-card'; const classes = [`${prefixCls}-list-wrapper`]; if (viewMode !== 'popup') { const wrapperClassName = this.getWrapperClassNames(); if (wrapperClassName) { classes.push(wrapperClassName); } } return ( <div className={classes.join(' ')}> {viewMode !== 'popup' && this.renderDragUploadArea()} {this.renderHeader(!isCard && uploadBtn)} {!__inGroup && viewMode !== 'popup' && this.renderHelp()} {!__inGroup && this.showValidation === ShowValidation.newLine && this.renderValidationResult()} {!__inGroup && this.renderEmpty()} {viewMode !== 'none' && this.renderUploadList(isCard && uploadBtn)} </div> ); } getPictureWidth() { const { pictureWidth, listType } = this.props; return pictureWidth || (listType === 'picture-card' ? 100 : 48); } @autobind handleHelpMouseEnter(e) { const { getTooltipTheme } = this.context; const { helpTooltipProps, help } = this; let helpTooltipCls = `${this.getContextConfig('proPrefixCls')}-tooltip-popup-help`; if (helpTooltipProps && helpTooltipProps.popupClassName) { helpTooltipCls = helpTooltipCls.concat(' ', helpTooltipProps.popupClassName) } show(e.currentTarget, { title: help, theme: getTooltipTheme('help'), placement: 'bottom', // 保留 bottom 原效果 ...helpTooltipProps, popupClassName: helpTooltipCls, }); } handleHelpMouseLeave() { hide(); } renderHelp(): ReactNode { const { help, showAttachmentHelp } = this; if (!help || showAttachmentHelp === ShowHelp.none) return; switch (showAttachmentHelp) { case ShowHelp.tooltip: return ( <Icon type="help" style={{ fontSize: '0.14rem', color: '#8c8c8c' }} onMouseEnter={this.handleHelpMouseEnter} onMouseLeave={this.handleHelpMouseLeave} /> ); default: return ( <div key="help" className={`${this.getContextProPrefixCls(FIELD_SUFFIX)}-help`}> {toJS(help)} </div> ); } } get showValidation() { const { viewMode, showValidation = viewMode === 'popup' ? ShowValidation.tooltip : ShowValidation.newLine } = this.props; const { context: { showValidation: ctxShowValidation = showValidation } } = this; return ctxShowValidation; } @autobind handlePopupHiddenChange(hidden) { this.setPopup(!hidden); } @mobxAction setPopup(popup) { this.popup = popup; } @mobxAction setDragState(state) { this.dragState = state; } @autobind handleFileDrop(e) { this.setDragState(e.type); } renderDefaultDragBox() { const { prefixCls, help } = this; return ( <div className={`${prefixCls}-drag-box`}> <p className={`${prefixCls}-drag-box-icon`}> <Icon type="inbox" /> </p> <p className={`${prefixCls}-drag-box-text`}>{$l('Attachment', 'file_type_mismatch')}</p> <p className={`${prefixCls}-drag-box-hint`}> {help} </p> </div> ); } renderDragUploadArea() { const { dragUpload, dragBoxRender, readOnly } = this.props; const { prefixCls, accept } = this; if (dragUpload) { const isDisabled = this.isDisabled() || readOnly; const dragCls = classNames(prefixCls, { [`${prefixCls}-drag`]: true, [`${prefixCls}-drag-hover`]: this.dragState === 'dragover', [`${prefixCls}-drag-disabled`]: isDisabled, }); const rcUploadProps = { ...this.props, disabled: isDisabled, accept: accept ? accept.join(',') : undefined, beforeUpload: this.handleDragUpload, prefixCls, }; return ( <div className={dragCls} onDrop={this.handleFileDrop} onDragOver={this.handleFileDrop} onDragLeave={this.handleFileDrop} > <RcUpload {...rcUploadProps} className={`${prefixCls}-btn`}> {dragBoxRender || this.renderDefaultDragBox()} </RcUpload> </div> ); } return undefined; } render() { const { viewMode, listType, hidden } = this.props; const { readOnly, prefixCls } = this; if (viewMode === 'popup') { const label = this.hasFloatLabel && this.getLabel(); return ( <> <Trigger prefixCls={prefixCls} popupContent={this.renderWrapper} action={[Action.hover, Action.focus]} builtinPlacements={BUILT_IN_PLACEMENTS} popupPlacement="bottomLeft" popupHidden={hidden || !this.popup} onPopupHiddenChange={this.handlePopupHiddenChange} > {this.renderDragUploadArea()} {readOnly ? this.renderViewButton(label) : this.renderUploadBtn(false, label)} </Trigger> {this.renderTemplateDownloadButton()} {this.renderHelp()} {this.showValidation === ShowValidation.newLine && this.renderValidationResult()} </> ); } return this.renderWrapperList(readOnly ? undefined : this.renderUploadBtn(listType === 'picture-card')); } }