mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-06 06:08:07 +08:00
refactor(client): add attachment file type registry (#5353)
* refactor(client): add attachment file type registry * refactor(client): change api name of attachmentFileTypes * fix(client): to open file preview in new window if previewer not defined * fix(client): fix attachmentFileTypes default value * refactor(client): simplify file previewer code and allow to preview most file by default * refactor(client): make custom file type has high priority * refactor(client): add types for upload component api * fix(client): fix upload component locale
This commit is contained in:
parent
512df745fa
commit
164847d9a6
@ -800,6 +800,7 @@
|
||||
"Try again": "重试一下",
|
||||
"Download logs": "下载日志",
|
||||
"Download": "下载",
|
||||
"File type is not supported for previewing, please download it to preview.": "不支持预览该文件类型,请下载后查看。",
|
||||
"Click or drag file to this area to upload": "点击或拖拽文件到此区域上传",
|
||||
"Support for a single or bulk upload.": "支持单个或批量上传",
|
||||
"File size should not exceed {{size}}.": "文件大小不能超过 {{size}}",
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { DeleteOutlined, DownloadOutlined, InboxOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Field } from '@formily/core';
|
||||
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
||||
import { Upload as AntdUpload, Button, Modal, Progress, Space, Tooltip } from 'antd';
|
||||
import { Alert, Upload as AntdUpload, Button, Modal, Progress, Space, Tooltip } from 'antd';
|
||||
import useUploadStyle from 'antd/es/upload/style';
|
||||
import cls from 'classnames';
|
||||
import { saveAs } from 'file-saver';
|
||||
@ -18,13 +18,14 @@ import filesize from 'filesize';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LightBox from 'react-image-lightbox';
|
||||
import match from 'mime-match';
|
||||
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
|
||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import {
|
||||
FILE_SIZE_LIMIT_DEFAULT,
|
||||
isImage,
|
||||
isPdf,
|
||||
attachmentFileTypes,
|
||||
getThumbnailPlaceholderURL,
|
||||
normalizeFile,
|
||||
toFileList,
|
||||
toValueItem,
|
||||
@ -34,6 +35,129 @@ import {
|
||||
import { useStyles } from './style';
|
||||
import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type';
|
||||
|
||||
attachmentFileTypes.add({
|
||||
match(file) {
|
||||
return match(file.mimetype || file.type, 'image/*');
|
||||
},
|
||||
getThumbnailURL(file) {
|
||||
return file.url ? `${file.url}${file.thumbnailRule || ''}` : URL.createObjectURL(file.originFileObj);
|
||||
},
|
||||
Previewer({ index, list, onSwitchIndex }) {
|
||||
const onDownload = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const file = list[index];
|
||||
saveAs(file.url, `${file.title}${file.extname}`);
|
||||
},
|
||||
[index, list],
|
||||
);
|
||||
return (
|
||||
<LightBox
|
||||
// discourageDownloads={true}
|
||||
mainSrc={list[index]?.url}
|
||||
nextSrc={list[(index + 1) % list.length]?.url}
|
||||
prevSrc={list[(index + list.length - 1) % list.length]?.url}
|
||||
onCloseRequest={() => onSwitchIndex(null)}
|
||||
onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)}
|
||||
onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)}
|
||||
imageTitle={list[index]?.title}
|
||||
toolbarButtons={[
|
||||
<button
|
||||
key={'preview-img'}
|
||||
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
|
||||
type="button"
|
||||
aria-label="Download"
|
||||
title="Download"
|
||||
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
|
||||
onClick={onDownload}
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'video/*'];
|
||||
|
||||
function IframePreviewer({ index, list, onSwitchIndex }) {
|
||||
const { t } = useTranslation();
|
||||
const file = list[index];
|
||||
const onOpen = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(file.url);
|
||||
},
|
||||
[file],
|
||||
);
|
||||
const onDownload = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
saveAs(file.url, `${file.title}${file.extname}`);
|
||||
},
|
||||
[file],
|
||||
);
|
||||
const onClose = useCallback(() => {
|
||||
onSwitchIndex(null);
|
||||
}, [onSwitchIndex]);
|
||||
return (
|
||||
<Modal
|
||||
open={index != null}
|
||||
title={file.title}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="open" onClick={onOpen}>
|
||||
{t('Open in new window')}
|
||||
</Button>,
|
||||
<Button key="download" onClick={onDownload}>
|
||||
{t('Download')}
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t('Close')}
|
||||
</Button>,
|
||||
]}
|
||||
width={'85vw'}
|
||||
centered={true}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: 'calc(100vh - 256px)',
|
||||
height: '90vh',
|
||||
width: '100%',
|
||||
background: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{iframePreviewSupportedTypes.some((type) => match(file.mimetype || file.extname, type)) ? (
|
||||
<iframe
|
||||
src={file.url}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxHeight: '90vh',
|
||||
flex: '1 1 auto',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
type="warning"
|
||||
description={t('File type is not supported for previewing, please download it to preview.')}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function InternalUpload(props: UploadProps) {
|
||||
const { onChange, ...rest } = props;
|
||||
const onFileChange = useCallback(
|
||||
@ -85,6 +209,13 @@ function useSizeHint(size: number) {
|
||||
return s !== 0 ? t('File size should not exceed {{size}}.', { size: sizeString }) : '';
|
||||
}
|
||||
|
||||
function DefaultThumbnailPreviewer({ file }) {
|
||||
const { componentCls: prefixCls } = useStyles();
|
||||
const { getThumbnailURL = getThumbnailPlaceholderURL } = attachmentFileTypes.getTypeByFile(file) ?? {};
|
||||
const imageUrl = getThumbnailURL(file);
|
||||
return <img src={imageUrl} alt={file.title} className={`${prefixCls}-list-item-image`} />;
|
||||
}
|
||||
|
||||
function AttachmentListItem(props) {
|
||||
const { file, disabled, onPreview, onDelete: propsOnDelete, readPretty } = props;
|
||||
const { componentCls: prefixCls } = useStyles();
|
||||
@ -104,15 +235,11 @@ function AttachmentListItem(props) {
|
||||
saveAs(file.url, `${file.title}${file.extname}`);
|
||||
}, [file]);
|
||||
|
||||
const { ThumbnailPreviewer = DefaultThumbnailPreviewer } = attachmentFileTypes.getTypeByFile(file) ?? {};
|
||||
|
||||
const item = [
|
||||
<span key="thumbnail" className={`${prefixCls}-list-item-thumbnail`}>
|
||||
{file.imageUrl && (
|
||||
<img
|
||||
src={`${file.imageUrl}${file.thumbnailRule || ''}`}
|
||||
alt={file.title}
|
||||
className={`${prefixCls}-list-item-image`}
|
||||
/>
|
||||
)}
|
||||
<ThumbnailPreviewer file={file} />
|
||||
</span>,
|
||||
<span key="title" className={`${prefixCls}-list-item-name`} title={file.title}>
|
||||
{file.status === 'uploading' ? t('Uploading') : file.title}
|
||||
@ -166,121 +293,12 @@ function AttachmentListItem(props) {
|
||||
);
|
||||
}
|
||||
|
||||
const PreviewerTypes = [
|
||||
{
|
||||
matcher: isImage,
|
||||
Component({ index, list, onSwitchIndex }) {
|
||||
const onDownload = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const file = list[index];
|
||||
saveAs(file.url, `${file.title}${file.extname}`);
|
||||
},
|
||||
[index, list],
|
||||
);
|
||||
return (
|
||||
<LightBox
|
||||
// discourageDownloads={true}
|
||||
mainSrc={list[index]?.imageUrl}
|
||||
nextSrc={list[(index + 1) % list.length]?.imageUrl}
|
||||
prevSrc={list[(index + list.length - 1) % list.length]?.imageUrl}
|
||||
onCloseRequest={() => onSwitchIndex(null)}
|
||||
onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)}
|
||||
onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)}
|
||||
imageTitle={list[index]?.title}
|
||||
toolbarButtons={[
|
||||
<button
|
||||
key={'preview-img'}
|
||||
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
|
||||
type="button"
|
||||
aria-label="Download"
|
||||
title="Download"
|
||||
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
|
||||
onClick={onDownload}
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
matcher: isPdf,
|
||||
Component({ index, list, onSwitchIndex }) {
|
||||
const { t } = useTranslation();
|
||||
const onDownload = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const file = list[index];
|
||||
saveAs(file.url, `${file.title}${file.extname}`);
|
||||
},
|
||||
[index, list],
|
||||
);
|
||||
const onClose = useCallback(() => {
|
||||
onSwitchIndex(null);
|
||||
}, [onSwitchIndex]);
|
||||
return (
|
||||
<Modal
|
||||
open={index != null}
|
||||
title={'PDF - ' + list[index].title}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button
|
||||
key="download"
|
||||
style={{
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
onClick={onDownload}
|
||||
>
|
||||
{t('Download')}
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose} style={{ textTransform: 'capitalize' }}>
|
||||
{t('Close')}
|
||||
</Button>,
|
||||
]}
|
||||
width={'85vw'}
|
||||
centered={true}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '8px',
|
||||
maxWidth: '100%',
|
||||
maxHeight: 'calc(100vh - 256px)',
|
||||
height: '90vh',
|
||||
width: '100%',
|
||||
background: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src={list[index].url}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxHeight: '90vh',
|
||||
flex: '1 1 auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function Previewer({ index, onSwitchIndex, list }) {
|
||||
if (index == null) {
|
||||
return null;
|
||||
}
|
||||
const file = list[index];
|
||||
const { Component } = PreviewerTypes.find((type) => type.matcher(file)) ?? {};
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
const { Previewer: Component = IframePreviewer } = attachmentFileTypes.getTypeByFile(file) ?? {};
|
||||
|
||||
return <Component index={index} list={list} onSwitchIndex={onSwitchIndex} />;
|
||||
}
|
||||
@ -298,14 +316,7 @@ export function AttachmentList(props) {
|
||||
const onPreview = useCallback(
|
||||
(file) => {
|
||||
const index = fileList.findIndex((item) => item.id === file.id);
|
||||
const previewType = PreviewerTypes.find((type) => type.matcher(file));
|
||||
if (previewType) {
|
||||
setPreview(index);
|
||||
} else {
|
||||
if (file.id) {
|
||||
saveAs(file.url, `${file.title}${file.extname}`);
|
||||
}
|
||||
}
|
||||
setPreview(index);
|
||||
},
|
||||
[fileList],
|
||||
);
|
||||
|
@ -8,3 +8,4 @@
|
||||
*/
|
||||
|
||||
export * from './Upload';
|
||||
export { attachmentFileTypes } from './shared';
|
||||
|
@ -11,20 +11,52 @@ import { isArr, isValid, toArr as toArray } from '@formily/shared';
|
||||
import { UploadFile } from 'antd/es/upload/interface';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import match from 'mime-match';
|
||||
import { useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useAPIClient } from '../../../api-client';
|
||||
import { UNKNOWN_FILE_ICON, UPLOAD_PLACEHOLDER } from './placeholder';
|
||||
import type { IUploadProps, UploadProps } from './type';
|
||||
|
||||
export const FILE_SIZE_LIMIT_DEFAULT = 1024 * 1024 * 20;
|
||||
|
||||
export const isImage = (file) => {
|
||||
return match(file.mimetype || file.type, 'image/*');
|
||||
};
|
||||
export interface FileModel {
|
||||
id: number;
|
||||
filename: string;
|
||||
path: string;
|
||||
title: string;
|
||||
url: string;
|
||||
extname: string;
|
||||
size: number;
|
||||
mimetype: string;
|
||||
}
|
||||
|
||||
export const isPdf = (file) => {
|
||||
return match(file.mimetype || file.type, 'application/pdf');
|
||||
};
|
||||
export interface PreviewerProps {
|
||||
index: number;
|
||||
list: FileModel[];
|
||||
onSwitchIndex(index): void;
|
||||
}
|
||||
|
||||
export interface AttachmentFileType {
|
||||
match(file: any): boolean;
|
||||
getThumbnailURL?(file: any): string;
|
||||
ThumbnailPreviewer?: React.ComponentType<{ file: FileModel }>;
|
||||
Previewer?: React.ComponentType<PreviewerProps>;
|
||||
}
|
||||
|
||||
export class AttachmentFileTypes {
|
||||
types: AttachmentFileType[] = [];
|
||||
add(type: AttachmentFileType) {
|
||||
// NOTE: use unshift to make sure the custom type has higher priority
|
||||
this.types.unshift(type);
|
||||
}
|
||||
getTypeByFile(file): Omit<AttachmentFileType, 'match'> {
|
||||
return this.types.find((type) => type.match(file));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export const attachmentFileTypes = new AttachmentFileTypes();
|
||||
|
||||
const toArr = (value) => {
|
||||
if (!isValid(value)) {
|
||||
@ -36,7 +68,7 @@ const toArr = (value) => {
|
||||
return toArray(value);
|
||||
};
|
||||
|
||||
export const testOpts = (ext: RegExp, options: { exclude?: string[]; include?: string[] }) => {
|
||||
const testOpts = (ext: RegExp, options: { exclude?: string[]; include?: string[] }) => {
|
||||
if (options && isArr(options.include)) {
|
||||
return options.include.some((url) => ext.test(url));
|
||||
}
|
||||
@ -48,26 +80,19 @@ export const testOpts = (ext: RegExp, options: { exclude?: string[]; include?: s
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getImageByUrl = (url: string, options: any = {}) => {
|
||||
export function getThumbnailPlaceholderURL(file, options: any = {}) {
|
||||
for (let i = 0; i < UPLOAD_PLACEHOLDER.length; i++) {
|
||||
// console.log(UPLOAD_PLACEHOLDER[i].ext, testOpts(UPLOAD_PLACEHOLDER[i].ext, options));
|
||||
if (UPLOAD_PLACEHOLDER[i].ext.test(url)) {
|
||||
if (UPLOAD_PLACEHOLDER[i].ext.test(file.extname || file.filename || file.url || file.name)) {
|
||||
if (testOpts(UPLOAD_PLACEHOLDER[i].ext, options)) {
|
||||
return UPLOAD_PLACEHOLDER[i].icon || UNKNOWN_FILE_ICON;
|
||||
} else {
|
||||
return url;
|
||||
return file.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return UNKNOWN_FILE_ICON;
|
||||
};
|
||||
|
||||
export const getURL = (target: any) => {
|
||||
return target?.['url'] || target?.['downloadURL'] || target?.['imgURL'] || target?.['name'];
|
||||
};
|
||||
export const getThumbURL = (target: any) => {
|
||||
return target?.['thumbUrl'] || target?.['url'] || target?.['downloadURL'] || target?.['imgURL'] || target?.['name'];
|
||||
};
|
||||
}
|
||||
|
||||
export function getResponseMessage({ error, response }: UploadFile<any>) {
|
||||
if (error instanceof Error && 'isAxiosError' in error) {
|
||||
@ -93,24 +118,14 @@ export function getResponseMessage({ error, response }: UploadFile<any>) {
|
||||
}
|
||||
|
||||
export function normalizeFile(file: UploadFile & Record<string, any>) {
|
||||
const imageUrl = isImage(file) ? URL.createObjectURL(file.originFileObj) : getImageByUrl(file.name);
|
||||
const response = getResponseMessage(file);
|
||||
return {
|
||||
...file,
|
||||
title: file.name,
|
||||
thumbUrl: imageUrl,
|
||||
imageUrl,
|
||||
response,
|
||||
};
|
||||
}
|
||||
|
||||
export const normalizeFileList = (fileList: UploadFile[]) => {
|
||||
if (fileList && fileList.length) {
|
||||
return fileList.map(normalizeFile);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export function useUploadProps<T extends IUploadProps = UploadProps>(props: T) {
|
||||
const api = useAPIClient();
|
||||
|
||||
@ -166,11 +181,6 @@ export const toItem = (file) => {
|
||||
...file,
|
||||
id: file.id || file.uid,
|
||||
title: file.title || file.name,
|
||||
imageUrl: isImage(file)
|
||||
? file.url
|
||||
: getImageByUrl(file.url, {
|
||||
exclude: ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp', '.ico'],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -178,12 +188,6 @@ export const toFileList = (fileList: any) => {
|
||||
return toArr(fileList).filter(Boolean).map(toItem);
|
||||
};
|
||||
|
||||
export const toValue = (fileList: any) => {
|
||||
return toArr(fileList)
|
||||
.filter((file) => !file.response || file.status === 'done')
|
||||
.map((file) => file?.response?.data || file);
|
||||
};
|
||||
|
||||
const Rules: Record<string, RuleFunction> = {
|
||||
size(file, options: number): null | string {
|
||||
const size = options ?? FILE_SIZE_LIMIT_DEFAULT;
|
||||
|
Loading…
Reference in New Issue
Block a user