add upload component into schema components (#165)

* feat: add Upload component into schema components

* docx file

* upload with customRequest

* action

* optimize: remove downloadFile instead of saveAs

* fix: direct download of non-image files

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
SemmyWong 2022-01-22 21:05:06 +08:00 committed by GitHub
parent 5e7e3c2c46
commit be1192531c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 983 additions and 2 deletions

View File

@ -25,6 +25,7 @@
"@formily/react": "^2.0.7",
"ahooks": "^3.0.5",
"axios": "^0.24.0",
"file-saver": "^2.0.5",
"i18next": "^21.6.0",
"react-helmet": "^6.1.0",
"react-i18next": "^11.15.1",

View File

@ -16,4 +16,4 @@ export * from './page';
export * from './password';
export * from './radio';
export * from './time-picker';
export * from './upload';

View File

@ -0,0 +1,147 @@
import DownloadOutlined from '@ant-design/icons/DownloadOutlined';
import { Field } from '@formily/core';
import { useField } from '@formily/react';
import { Button, Space } from 'antd';
import cls from 'classnames';
import { saveAs } from 'file-saver';
import React, { useState } from 'react';
import Lightbox from 'react-image-lightbox';
import { isImage, toArr, toImages } from './shared';
import type { UploadProps } from './type';
type Composed = React.FC<UploadProps> & {
Upload?: React.FC<UploadProps>;
Attachment?: React.FC<UploadProps>;
};
export const ReadPretty: Composed = () => null;
ReadPretty.Attachment = (props: UploadProps) => {
const field = useField<Field>();
const images = toImages(toArr(field.value));
const [photoIndex, setPhotoIndex] = useState(0);
const [visible, setVisible] = useState(false);
const { size } = props;
console.log('field.value', field.value, images);
return (
<div>
<div className={cls('ant-upload-picture-card-wrapper nb-upload', size ? `nb-upload-${size}` : null)}>
<div className={'ant-upload-list ant-upload-list-picture-card'}>
{images.map((file) => {
const handleClick = (e) => {
e.preventDefault();
e.stopPropagation();
const index = images.indexOf(file);
if (isImage(file.extname)) {
setVisible(true);
setPhotoIndex(index);
} else {
saveAs(file.url, `${file.title}${file.extname}`);
}
};
return (
<div className={'ant-upload-list-picture-card-container'}>
<div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
<div className={'ant-upload-list-item-info'}>
<span className="ant-upload-span">
<a
className="ant-upload-list-item-thumbnail"
href={file.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
>
{file.imageUrl && (
<img
src={`${file.imageUrl}?x-oss-process=style/thumbnail`}
alt={file.title}
className="ant-upload-list-item-image"
/>
)}
</a>
<a
target="_blank"
rel="noopener noreferrer"
className="ant-upload-list-item-name"
title={file.title}
href={file.url}
onClick={handleClick}
>
{file.title}
</a>
</span>
</div>
{size !== 'small' && (
<span className={'ant-upload-list-item-actions'}>
<Space size={3}>
<Button
size={'small'}
type={'text'}
icon={<DownloadOutlined />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
saveAs(file.url, `${file.title}${file.extname}`);
}}
/>
</Space>
</span>
)}
</div>
</div>
);
})}
</div>
</div>
{visible && (
<Lightbox
// discourageDownloads={true}
mainSrc={images[photoIndex]?.imageUrl}
nextSrc={images[(photoIndex + 1) % images.length]?.imageUrl}
prevSrc={images[(photoIndex + images.length - 1) % images.length]?.imageUrl}
// @ts-ignore
onCloseRequest={(e) => {
e.preventDefault();
e.stopPropagation();
setVisible(false);
}}
onMovePrevRequest={() => setPhotoIndex((photoIndex + images.length - 1) % images.length)}
onMoveNextRequest={() => setPhotoIndex((photoIndex + 1) % images.length)}
imageTitle={images[photoIndex]?.title}
toolbarButtons={[
<button
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
type="button"
aria-label="Zoom in"
title="Zoom in"
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const file = images[photoIndex];
saveAs(file.url, `${file.title}${file.extname}`);
}}
>
<DownloadOutlined />
</button>,
]}
/>
)}
</div>
);
};
ReadPretty.Upload = (props) => {
const field = useField<Field>();
return (field.value || []).map((item) => (
<div>
{item.url ? (
<a target={'_blank'} href={item.url}>
{item.name}
</a>
) : (
<span>{item.name}</span>
)}
</div>
));
};

View File

@ -0,0 +1,207 @@
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import DownloadOutlined from '@ant-design/icons/DownloadOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import { usePrefixCls } from '@formily/antd/esm/__builtins__';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Button, Progress, Space, Upload as AntdUpload } from 'antd';
import cls from 'classnames';
import { saveAs } from 'file-saver';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { ReadPretty } from './ReadPretty';
import { isImage, toArr, toFileList, toItem, toValue, useUploadProps } from './shared';
import './style.less';
import type { ComposedUpload, DraggerProps, UploadProps } from './type';
export const Upload: ComposedUpload = connect(
(props: UploadProps) => {
return <AntdUpload {...useUploadProps(props)} />;
},
mapProps({
value: 'fileList',
}),
mapReadPretty(ReadPretty.Upload),
);
Upload.Attachment = connect((props: UploadProps) => {
const { disabled, multiple, value, onChange } = props;
const [fileList, setFileList] = useState([]);
const [sync, setSync] = useState(true);
const images = fileList;
const [photoIndex, setPhotoIndex] = useState(0);
const [visible, setVisible] = useState(false);
const { t } = useTranslation();
useEffect(() => {
if (sync) {
setFileList(toFileList(value));
}
}, [value, sync]);
return (
<div>
<div className={cls('ant-upload-picture-card-wrapper nb-upload')}>
<div className={'ant-upload-list ant-upload-list-picture-card'}>
{fileList.map((file) => {
const handleClick = (e) => {
e.preventDefault();
e.stopPropagation();
const index = fileList.indexOf(file);
if (isImage(file.extname)) {
setVisible(true);
setPhotoIndex(index);
} else {
saveAs(file.url, `${file.title}${file.extname}`);
}
};
return (
<div className={'ant-upload-list-picture-card-container'}>
<div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
<div className={'ant-upload-list-item-info'}>
<span className="ant-upload-span">
<a
className="ant-upload-list-item-thumbnail"
href={file.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
>
{file.imageUrl && (
<img src={file.imageUrl} alt={file.title} className="ant-upload-list-item-image" />
)}
</a>
<a
target="_blank"
rel="noopener noreferrer"
className="ant-upload-list-item-name"
title={file.title}
href={file.url}
onClick={handleClick}
>
{file.status === 'uploading' ? t('Uploading') : file.title}
</a>
</span>
</div>
<span className={'ant-upload-list-item-actions'}>
<Space size={3}>
<Button
size={'small'}
type={'text'}
icon={<DownloadOutlined />}
onClick={() => {
saveAs(file.url, `${file.title}${file.extname}`);
}}
/>
{!disabled && (
<Button
size={'small'}
type={'text'}
icon={<DeleteOutlined />}
onClick={() => {
setSync(false);
setFileList((prevFileList) => {
if (!multiple) {
onChange(null);
return [];
}
const index = prevFileList.indexOf(file);
prevFileList.splice(index, 1);
onChange(toValue([...prevFileList]));
return [...prevFileList];
});
}}
/>
)}
</Space>
</span>
{file.status === 'uploading' && (
<div className={'ant-upload-list-item-progress'}>
<Progress strokeWidth={2} type={'line'} showInfo={false} percent={file.percent} />
</div>
)}
</div>
</div>
);
})}
<div className={'ant-upload-list-picture-card-container'}>
<AntdUpload
{...useUploadProps({ ...props })}
disabled={disabled}
multiple={multiple}
listType={'picture-card'}
fileList={fileList}
onChange={(info) => {
setSync(false);
if (multiple) {
if (info.file.status === 'done') {
onChange(toValue(info.fileList));
}
setFileList(info.fileList.map(toItem));
} else {
if (info.file.status === 'done') {
console.log('field.value', info.file?.response?.data);
// TODO(BUG): object 的联动有问题,不响应,折中的办法先置空再赋值
onChange(null);
onChange(info.file?.response?.data);
}
setFileList([toItem(info.file)]);
}
}}
showUploadList={false}
>
{!disabled && (multiple || toArr(value).length < 1) && (
<span>
<PlusOutlined />
<br /> {t('Upload')}
</span>
)}
</AntdUpload>
</div>
</div>
</div>
{visible && (
<Lightbox
// discourageDownloads={true}
mainSrc={images[photoIndex]?.imageUrl}
nextSrc={images[(photoIndex + 1) % images.length]?.imageUrl}
prevSrc={images[(photoIndex + images.length - 1) % images.length]?.imageUrl}
onCloseRequest={() => setVisible(false)}
onMovePrevRequest={() => setPhotoIndex((photoIndex + images.length - 1) % images.length)}
onMoveNextRequest={() => setPhotoIndex((photoIndex + 1) % images.length)}
imageTitle={images[photoIndex]?.title}
toolbarButtons={[
<button
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
type="button"
aria-label="Zoom in"
title="Zoom in"
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
onClick={(e) => {
e.preventDefault();
const file = images[photoIndex];
saveAs(file.url, `${file.title}${file.extname}`);
}}
>
<DownloadOutlined />
</button>,
]}
/>
)}
</div>
);
}, mapReadPretty(ReadPretty.Attachment));
Upload.Dragger = connect(
(props: DraggerProps) => {
return (
<div className={usePrefixCls('upload-dragger')}>
<AntdUpload.Dragger {...useUploadProps(props)} />
</div>
);
},
mapProps({
value: 'fileList',
}),
);
export default Upload;

View File

@ -0,0 +1,43 @@
import { APIClient } from '@nocobase/client';
import MockAdapter from 'axios-mock-adapter';
export const apiClient = new APIClient({
baseURL: 'http://localhost:8001/api/',
});
const mock = new MockAdapter(apiClient.axios);
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
mock.onPost('/attachments:upload').reply(async (config) => {
const total = 1024; // mocked file size
for (const progress of [0, 0.2, 0.4, 0.6, 0.8, 1]) {
await sleep(500);
if (config.onUploadProgress) {
config.onUploadProgress({ loaded: total * progress, total });
}
}
return [
200,
{
data: {
id: 2,
title: 'd9f6ad6669902a9a8a1229d9f362235a (6)',
filename: '7edb55e4e3145e5ac59ea3a44ca840e9.docx',
extname: '.docx',
path: '',
size: null,
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/7edb55e4e3145e5ac59ea3a44ca840e9.docx',
mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
meta: {},
storage_id: 2,
updated_at: '2022-01-21T07:21:21.084Z',
created_at: '2022-01-21T07:21:21.084Z',
created_by_id: null,
updated_by_id: null,
},
},
];
});
export default apiClient;

View File

@ -0,0 +1,51 @@
/**
* title: Upload
*/
import { FormItem } from '@formily/antd';
import { APIClientProvider, SchemaComponent, SchemaComponentProvider, Upload } from '@nocobase/client';
import React from 'react';
import apiClient from './apiClient';
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'Upload.Attachment',
'x-component-props': {
action: 'attachments:upload',
// multiple: true,
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Upload.Attachment',
'x-component-props': {
// multiple: true,
},
},
},
};
export default () => {
return (
<APIClientProvider apiClient={apiClient}>
<SchemaComponentProvider components={{ Upload, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</APIClientProvider>
);
};

View File

@ -0,0 +1,115 @@
/**
* title: Upload
*/
import { FormItem } from '@formily/antd';
import { APIClientProvider, SchemaComponent, SchemaComponentProvider, Upload } from '@nocobase/client';
import React from 'react';
import apiClient from './apiClient';
const schema = {
type: 'object',
properties: {
input: {
type: 'object',
title: `编辑模式`,
default: [
{
id: 45,
title: 's33766399',
name: 's33766399',
filename: 'cd48dc833ab01aa3959ac39309fc39de.jpg',
extname: '.jpg',
size: null,
mimetype: 'image/jpeg',
path: '',
meta: {},
status: 'uploading',
percent: 60,
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/cd48dc833ab01aa3959ac39309fc39de.jpg',
created_at: '2021-08-13T15:00:17.423Z',
updated_at: '2021-08-13T15:00:17.423Z',
created_by_id: null,
updated_by_id: null,
storage_id: 2,
},
{
id: 7,
title: '简历',
filename: 'd9f6ad6669902a9a8a1229d9f362235a.docx',
extname: '.docx',
size: null,
mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
path: '',
meta: {},
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/d9f6ad6669902a9a8a1229d9f362235a.docx',
created_at: '2021-09-12T01:22:06.229Z',
updated_at: '2021-09-12T01:22:06.229Z',
created_by_id: null,
updated_by_id: 1,
storage_id: 2,
t_jh7a28dsfzi: {
createdAt: '2021-09-12T01:22:07.886Z',
updatedAt: '2021-09-12T01:22:07.886Z',
f_xg3mysbjfra: 1,
f_gc7ppj0b7n1: 7,
},
},
],
'x-decorator': 'FormItem',
'x-component': 'Upload.Attachment',
'x-component-props': {
action: 'attachments:upload',
multiple: true,
},
'x-reactions': [
{
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
{
target: 'read2',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
],
},
read: {
type: 'object',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Upload.Attachment',
'x-component-props': {
multiple: true,
},
},
read2: {
type: 'object',
title: `小图预览`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Upload.Attachment',
'x-component-props': {
multiple: true,
size: 'small',
},
},
},
};
export default () => {
return (
<APIClientProvider apiClient={apiClient}>
<SchemaComponentProvider components={{ Upload, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</APIClientProvider>
);
};

View File

@ -6,3 +6,13 @@ group:
---
# Upload
## Examples
### Upload
<code src="./demos/demo1.tsx" />
### Upload
<code src="./demos/demo2.tsx" />

View File

@ -0,0 +1 @@
export * from './Upload';

View File

@ -0,0 +1,62 @@
export const UPLOAD_PLACEHOLDER = [
{
ext: /\.docx?$/i,
icon: '//img.alicdn.com/tfs/TB1n8jfr1uSBuNjy1XcXXcYjFXa-200-200.png',
},
{
ext: /\.pptx?$/i,
icon: '//img.alicdn.com/tfs/TB1ItgWr_tYBeNjy1XdXXXXyVXa-200-200.png',
},
{
ext: /\.jpe?g$/i,
icon: '//img.alicdn.com/tfs/TB1wrT5r9BYBeNjy0FeXXbnmFXa-200-200.png',
},
{
ext: /\.pdf$/i,
icon: '//img.alicdn.com/tfs/TB1GwD8r9BYBeNjy0FeXXbnmFXa-200-200.png',
},
{
ext: /\.png$/i,
icon: '//img.alicdn.com/tfs/TB1BHT5r9BYBeNjy0FeXXbnmFXa-200-200.png',
},
{
ext: /\.eps$/i,
icon: '//img.alicdn.com/tfs/TB1G_iGrVOWBuNjy0FiXXXFxVXa-200-200.png',
},
{
ext: /\.ai$/i,
icon: '//img.alicdn.com/tfs/TB1B2cVr_tYBeNjy1XdXXXXyVXa-200-200.png',
},
{
ext: /\.gif$/i,
icon: '//img.alicdn.com/tfs/TB1DTiGrVOWBuNjy0FiXXXFxVXa-200-200.png',
},
{
ext: /\.svg$/i,
icon: '//img.alicdn.com/tfs/TB1uUm9rY9YBuNjy0FgXXcxcXXa-200-200.png',
},
{
ext: /\.xlsx?$/i,
icon: '//img.alicdn.com/tfs/TB1any1r1OSBuNjy0FdXXbDnVXa-200-200.png',
},
{
ext: /\.psd?$/i,
icon: '//img.alicdn.com/tfs/TB1_nu1r1OSBuNjy0FdXXbDnVXa-200-200.png',
},
{
ext: /\.(wav|aif|aiff|au|mp1|mp2|mp3|ra|rm|ram|mid|rmi)$/i,
icon: '//img.alicdn.com/tfs/TB1jPvwr49YBuNjy0FfXXXIsVXa-200-200.png',
},
{
ext: /\.(avi|wmv|mpg|mpeg|vob|dat|3gp|mp4|mkv|rm|rmvb|mov|flv)$/i,
icon: '//img.alicdn.com/tfs/TB1FrT5r9BYBeNjy0FeXXbnmFXa-200-200.png',
},
{
ext: /\.(zip|rar|arj|z|gz|iso|jar|ace|tar|uue|dmg|pkg|lzh|cab)$/i,
icon: '//img.alicdn.com/tfs/TB10jmfr29TBuNjy0FcXXbeiFXa-200-200.png',
},
{
ext: /\.[^.]+$/i,
icon: '//img.alicdn.com/tfs/TB10.R4r3mTBuNjy1XbXXaMrVXa-200-200.png',
},
];

View File

@ -0,0 +1,228 @@
import { Field } from '@formily/core';
import { useField } from '@formily/react';
import { reaction } from '@formily/reactive';
import { isArr, isValid, toArr as toArray } from '@formily/shared';
import { useAPIClient } from '@nocobase/client';
import { UploadChangeParam } from 'antd/lib/upload';
import { UploadFile } from 'antd/lib/upload/interface';
import { useEffect } from 'react';
import { UPLOAD_PLACEHOLDER } from './placeholder';
import type { IUploadProps, UploadProps } from './type';
export const isImage = (extName: string) => {
var reg = /\.(png|jpg|gif|jpeg|webp)$/;
return reg.test(extName);
};
export const toMap = (fileList: any) => {
if (!fileList) {
return [];
}
if (typeof fileList !== 'object') {
return [];
}
let list = fileList;
if (!Array.isArray(fileList)) {
if (Object.keys({ ...fileList }).length === 0) {
return [];
}
list = [fileList];
}
console.log({ list, fileList });
return list.map((item) => {
return [item.id || item.uid, toItem(item)];
});
};
export const toImages = (fileList) => {
if (!fileList) {
return [];
}
if (typeof fileList !== 'object') {
return [];
}
if (Object.keys(fileList).length === 0) {
return [];
}
let list = fileList;
if (!Array.isArray(fileList) && typeof fileList === 'object') {
list = [fileList];
}
return list.map((item) => {
return {
...item,
title: item.title || item.name,
imageUrl: getImageByUrl(item.url, {
exclude: ['.png', '.jpg', '.jpeg', '.gif'],
}),
};
});
};
export const toArr = (value) => {
if (!isValid(value)) {
return [];
}
if (Object.keys(value).length === 0) {
return [];
}
return toArray(value);
};
export const testOpts = (ext: RegExp, options: { exclude?: string[]; include?: string[] }) => {
if (options && isArr(options.include)) {
return options.include.some((url) => ext.test(url));
}
if (options && isArr(options.exclude)) {
return !options.exclude.some((url) => ext.test(url));
}
return true;
};
export const getImageByUrl = (url: string, options: any) => {
for (let i = 0; i < UPLOAD_PLACEHOLDER.length; i++) {
if (UPLOAD_PLACEHOLDER[i].ext.test(url) && testOpts(UPLOAD_PLACEHOLDER[i].ext, options)) {
return UPLOAD_PLACEHOLDER[i].icon || url;
}
}
return url;
};
export const getURL = (target: any) => {
return target?.['url'] || target?.['downloadURL'] || target?.['imgURL'];
};
export const getThumbURL = (target: any) => {
return target?.['thumbUrl'] || target?.['url'] || target?.['downloadURL'] || target?.['imgURL'];
};
export const getErrorMessage = (target: any) => {
return target?.errorMessage ||
target?.errMsg ||
target?.errorMsg ||
target?.message ||
typeof target?.error === 'string'
? target.error
: '';
};
export const getState = (target: any) => {
if (target?.success === false) return 'error';
if (target?.failed === true) return 'error';
if (target?.error) return 'error';
return target?.state || target?.status;
};
export const normalizeFileList = (fileList: UploadFile[]) => {
if (fileList && fileList.length) {
return fileList.map((file, index) => {
return {
...file,
uid: file.uid || `${index}`,
status: getState(file.response) || getState(file),
url: getURL(file) || getURL(file?.response),
thumbUrl: getImageByUrl(getThumbURL(file) || getThumbURL(file?.response), {
exclude: ['.png', '.jpg', '.jpeg', '.gif'],
}),
};
});
}
return [];
};
export const useValidator = (validator: (value: any) => string) => {
const field = useField<Field>();
useEffect(() => {
const dispose = reaction(
() => field.value,
(value) => {
const message = validator(value);
field.setFeedback({
type: 'error',
code: 'UploadError',
messages: message ? [message] : [],
});
},
);
return () => {
dispose();
};
}, []);
};
export const useUploadValidator = (serviceErrorMessage = 'Upload Service Error') => {
useValidator((value) => {
const list = toArr(value);
for (let i = 0; i < list.length; i++) {
if (list[i]?.status === 'error') {
return getErrorMessage(list[i]?.response) || getErrorMessage(list[i]) || serviceErrorMessage;
}
}
});
};
export function useUploadProps<T extends IUploadProps = UploadProps>({ serviceErrorMessage, ...props }: T) {
useUploadValidator(serviceErrorMessage);
const onChange = (param: UploadChangeParam<UploadFile>) => {
props.onChange?.(normalizeFileList([...param.fileList]));
};
const api = useAPIClient();
return {
...props,
customRequest({ action, data, file, filename, headers, onError, onProgress, onSuccess, withCredentials }) {
const formData = new FormData();
if (data) {
Object.keys(data).forEach((key) => {
formData.append(key, data[key]);
});
}
formData.append(filename, file);
api.axios
.post(action, formData, {
withCredentials,
headers,
onUploadProgress: ({ total, loaded }) => {
onProgress({ percent: Math.round((loaded / total) * 100).toFixed(2) }, file);
},
})
.then(({ data }) => {
onSuccess(data, file);
})
.catch(onError);
return {
abort() {
console.log('upload progress is aborted.');
},
};
},
onChange,
};
}
export const toItem = (file) => {
if (file?.response?.data) {
file = file.response.data;
}
return {
...file,
id: file.id || file.uid,
title: file.title || file.name,
imageUrl: getImageByUrl(file.url, {
exclude: ['.png', '.jpg', '.jpeg', '.gif'],
}),
};
};
export const toFileList = (fileList: any) => {
return toArr(fileList).map(toItem);
};
export const toValue = (fileList: any) => {
return toArr(fileList)
.filter((file) => !file.response || file.status === 'done')
.map((file) => file?.response?.data || file);
};

View File

@ -0,0 +1,57 @@
.nb-file-list {
display: flex;
}
.nb-upload {
.ant-upload-list-item.ant-upload-list-item-list-type-picture-card {
padding: 3px;
}
.ant-upload-list-item-thumbnail {
img {
object-fit: cover;
}
}
.ant-upload-list-item-actions {
left: auto;
right: 4px;
top: 4px;
transform: none;
}
.ant-upload-list-picture-card .ant-upload-list-item-info {
overflow: inherit;
}
.ant-upload-list-picture-card .ant-upload-list-item-name {
display: block;
margin-top: 10px;
font-size: 13px;
color: #636363;
}
.ant-upload-list-picture-card .ant-upload-list-item:hover .ant-upload-list-item-info::before {
display: none;
}
.ant-upload-list-picture-card .ant-upload-list-item-progress {
bottom: calc(50% - 11px);
pointer-events: none;
}
.ant-btn {
background: rgba(0, 0, 0, 0.5);
}
.ant-upload-list-picture-card-container {
margin-bottom: 28px;
}
}
.nb-upload-small {
.ant-upload-list-picture-card-container {
margin: 0 3px 3px 0;
height: 32px;
width: 32px;
}
.ant-upload-list-picture-card .ant-upload-list-item-name {
display: none;
}
.ant-upload-list-picture .ant-upload-list-item,
.ant-upload-list-picture-card .ant-upload-list-item {
padding: 1px;
}
}

View File

@ -0,0 +1,26 @@
import type { DraggerProps as AntdDraggerProps, UploadProps as AntdUploadProps } from 'antd/lib/upload';
import { UploadFile } from 'antd/lib/upload/interface';
import React from 'react';
export type UploadProps = Omit<AntdUploadProps, 'onChange'> & {
onChange?: (fileList: UploadFile[]) => void;
serviceErrorMessage?: string;
value?: any;
size?: string;
};
export type DraggerProps = Omit<AntdDraggerProps, 'onChange'> & {
onChange?: (fileList: UploadFile[]) => void;
serviceErrorMessage?: string;
};
export type ComposedUpload = React.FC<UploadProps> & {
Dragger?: React.FC<DraggerProps>;
File?: React.FC<UploadProps>;
Attachment?: React.FC<UploadProps>;
};
export type IUploadProps = {
serviceErrorMessage?: string;
onChange?: (...args: any) => void;
};

View File

@ -6270,6 +6270,11 @@ execa@^5.0.0:
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.npmmirror.com/exenv/download/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -6443,6 +6448,11 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-saver@^2.0.5:
version "2.0.5"
resolved "https://registry.npmmirror.com/file-saver/download/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
file-type@^3.3.0:
version "3.9.0"
resolved "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
@ -12343,6 +12353,14 @@ react-i18next@^11.15.1:
html-escaper "^2.0.2"
html-parse-stringify "^3.0.1"
react-image-lightbox@^5.1.4:
version "5.1.4"
resolved "https://registry.nlark.com/react-image-lightbox/download/react-image-lightbox-5.1.4.tgz#5b847dcb79e9efdf9d7cd5621a92e0f156d2cf30"
integrity sha1-W4R9y3np79+dfNViGpLg8VbSzzA=
dependencies:
prop-types "^15.7.2"
react-modal "^3.11.1"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -12353,6 +12371,21 @@ react-is@^17.0.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-lifecycles-compat@^3.0.0:
version "3.0.4"
resolved "https://registry.npmmirror.com/react-lifecycles-compat/download/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha1-TxonOv38jzSIqMUWv9p4+HI1I2I=
react-modal@^3.11.1:
version "3.14.4"
resolved "https://registry.npmmirror.com/react-modal/download/react-modal-3.14.4.tgz#2ca7e8e9a180955e5c9508c228b73167c1e6f6a3"
integrity sha512-8surmulejafYCH9wfUmFyj4UfbSJwjcgbS9gf3oOItu4Hwd6ivJyVBETI0yHRhpJKCLZMUtnhzk76wXTsNL6Qg==
dependencies:
exenv "^1.2.0"
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-refresh@0.10.0:
version "0.10.0"
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3"
@ -14934,7 +14967,7 @@ walker@^1.0.7, walker@~1.0.5:
dependencies:
makeerror "1.0.12"
warning@^4.0.1:
warning@^4.0.1, warning@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==