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:
parent
5e7e3c2c46
commit
be1192531c
@ -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",
|
||||
|
@ -16,4 +16,4 @@ export * from './page';
|
||||
export * from './password';
|
||||
export * from './radio';
|
||||
export * from './time-picker';
|
||||
|
||||
export * from './upload';
|
||||
|
147
packages/client/src/schema-component/antd/upload/ReadPretty.tsx
Normal file
147
packages/client/src/schema-component/antd/upload/ReadPretty.tsx
Normal 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>
|
||||
));
|
||||
};
|
207
packages/client/src/schema-component/antd/upload/Upload.tsx
Normal file
207
packages/client/src/schema-component/antd/upload/Upload.tsx
Normal 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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
115
packages/client/src/schema-component/antd/upload/demos/demo2.tsx
Normal file
115
packages/client/src/schema-component/antd/upload/demos/demo2.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -6,3 +6,13 @@ group:
|
||||
---
|
||||
|
||||
# Upload
|
||||
|
||||
## Examples
|
||||
|
||||
### Upload
|
||||
|
||||
<code src="./demos/demo1.tsx" />
|
||||
|
||||
### Upload
|
||||
|
||||
<code src="./demos/demo2.tsx" />
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './Upload';
|
@ -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',
|
||||
},
|
||||
];
|
228
packages/client/src/schema-component/antd/upload/shared.ts
Normal file
228
packages/client/src/schema-component/antd/upload/shared.ts
Normal 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);
|
||||
};
|
57
packages/client/src/schema-component/antd/upload/style.less
Normal file
57
packages/client/src/schema-component/antd/upload/style.less
Normal 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;
|
||||
}
|
||||
}
|
26
packages/client/src/schema-component/antd/upload/type.d.ts
vendored
Normal file
26
packages/client/src/schema-component/antd/upload/type.d.ts
vendored
Normal 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;
|
||||
};
|
35
yarn.lock
35
yarn.lock
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user