improve upload component
This commit is contained in:
parent
15105c1efb
commit
a635c955c6
@ -23,6 +23,7 @@
|
||||
"@types/react-dom": "^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/koa": "^2.13.1",
|
||||
"@types/lodash": "^4.14.169",
|
||||
|
@ -29,6 +29,7 @@
|
||||
"axios": "^0.21.1",
|
||||
"beautiful-react-hooks": "^0.35.0",
|
||||
"constate": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"flat": "^5.0.2",
|
||||
"html-react-parser": "^1.2.7",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -5,10 +5,13 @@ const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'object',
|
||||
type: 'string',
|
||||
title: `编辑模式`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Upload.File',
|
||||
'x-component': 'Upload.Attachment',
|
||||
'x-component-props': {
|
||||
// multiple: true,
|
||||
},
|
||||
'x-reactions': {
|
||||
target: 'read',
|
||||
fulfill: {
|
||||
@ -19,11 +22,14 @@ const schema = {
|
||||
},
|
||||
},
|
||||
read: {
|
||||
type: 'object',
|
||||
type: 'string',
|
||||
title: `阅读模式`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Upload.File',
|
||||
'x-component': 'Upload.Attachment',
|
||||
'x-component-props': {
|
||||
// multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -7,30 +7,72 @@ const schema = {
|
||||
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,
|
||||
},
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Upload.File',
|
||||
'x-component': 'Upload.Attachment',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
},
|
||||
'x-reactions': {
|
||||
target: 'read',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
'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.File',
|
||||
'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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
observer,
|
||||
useField,
|
||||
} from '@formily/react';
|
||||
import { Button, Progress, Upload as AntdUpload } from 'antd';
|
||||
import { Button, Progress, Space, Upload as AntdUpload } from 'antd';
|
||||
import {
|
||||
UploadChangeParam,
|
||||
UploadProps as AntdUploadProps,
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from 'antd/lib/upload';
|
||||
import { reaction } from '@formily/reactive';
|
||||
import { UploadFile } from 'antd/lib/upload/interface';
|
||||
import { isArr, toArr } from '@formily/shared';
|
||||
import { isArr, toArr as toArray, isValid } from '@formily/shared';
|
||||
import { UPLOAD_PLACEHOLDER } from './placeholder';
|
||||
import { usePrefixCls } from '@formily/antd/esm/__builtins__';
|
||||
import { useState } from 'react';
|
||||
@ -23,10 +23,26 @@ import 'react-image-lightbox/style.css'; // This only needs to be imported once
|
||||
import { useMap } from 'ahooks';
|
||||
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
|
||||
import UploadOutlined from '@ant-design/icons/UploadOutlined';
|
||||
import PlusOutlined from '@ant-design/icons/PlusOutlined';
|
||||
import DownloadOutlined from '@ant-design/icons/DownloadOutlined';
|
||||
import './style.less';
|
||||
import cls from 'classnames';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
const toArr = (value) => {
|
||||
if (!isValid(value)) {
|
||||
return [];
|
||||
}
|
||||
if (Object.keys(value).length === 0) {
|
||||
return [];
|
||||
}
|
||||
return toArray(value);
|
||||
};
|
||||
|
||||
type UploadProps = Omit<AntdUploadProps, 'onChange'> & {
|
||||
onChange?: (fileList: UploadFile[]) => void;
|
||||
serviceErrorMessage?: string;
|
||||
value?: any;
|
||||
};
|
||||
|
||||
type DraggerProps = Omit<AntdDraggerProps, 'onChange'> & {
|
||||
@ -37,6 +53,7 @@ type DraggerProps = Omit<AntdDraggerProps, 'onChange'> & {
|
||||
type ComposedUpload = React.FC<UploadProps> & {
|
||||
Dragger?: React.FC<DraggerProps>;
|
||||
File?: React.FC<UploadProps>;
|
||||
Attachment?: React.FC<UploadProps>;
|
||||
};
|
||||
|
||||
type IUploadProps = {
|
||||
@ -221,6 +238,16 @@ function toItem(file) {
|
||||
};
|
||||
}
|
||||
|
||||
function toFileList(fileList: any) {
|
||||
return toArr(fileList).map(toItem);
|
||||
}
|
||||
|
||||
function toValue(fileList: any) {
|
||||
return toArr(fileList)
|
||||
.filter((file) => !file.response || file.status === 'done')
|
||||
.map((file) => file?.response?.data || file);
|
||||
}
|
||||
|
||||
function toMap(fileList: any) {
|
||||
if (!fileList) {
|
||||
return [];
|
||||
@ -228,13 +255,14 @@ function toMap(fileList: any) {
|
||||
if (typeof fileList !== 'object') {
|
||||
return [];
|
||||
}
|
||||
if (Object.keys(fileList).length === 0) {
|
||||
return [];
|
||||
}
|
||||
let list = fileList;
|
||||
if (!Array.isArray(fileList) && typeof fileList === 'object') {
|
||||
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)];
|
||||
});
|
||||
@ -281,7 +309,7 @@ Upload.File = connect(
|
||||
useEffect(() => {
|
||||
setAll(toMap(value));
|
||||
}, [value]);
|
||||
console.log('value', value);
|
||||
console.log('mapmapmapmapmap', map);
|
||||
return (
|
||||
<div>
|
||||
<AntdUpload
|
||||
@ -289,10 +317,11 @@ Upload.File = connect(
|
||||
{...useUploadProps({ ...others, multiple })}
|
||||
onChange={({ file }) => {
|
||||
console.log({ multiple, file });
|
||||
const f = toItem(file);
|
||||
if (multiple) {
|
||||
set(file.uid, toItem(file));
|
||||
set(f.id, f);
|
||||
} else {
|
||||
setAll([[file.uid, toItem(file)]]);
|
||||
setAll([[f.id, f]]);
|
||||
}
|
||||
if (file.status === 'done') {
|
||||
if (multiple) {
|
||||
@ -432,4 +461,294 @@ Upload.File = connect(
|
||||
|
||||
Upload.Dragger = Dragger;
|
||||
|
||||
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);
|
||||
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();
|
||||
const index = fileList.indexOf(file);
|
||||
setVisible(true);
|
||||
setPhotoIndex(index);
|
||||
};
|
||||
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'
|
||||
? '上传中...'
|
||||
: 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}
|
||||
action={`${process.env.API_URL}attachments:upload`}
|
||||
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 /> 上传
|
||||
</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={() => {
|
||||
const file = images[photoIndex];
|
||||
saveAs(file.url, `${file.title}${file.extname}`);
|
||||
}}
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
mapReadPretty((props) => {
|
||||
const field = useField<Formily.Core.Models.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();
|
||||
const index = images.indexOf(file);
|
||||
setVisible(true);
|
||||
setPhotoIndex(index);
|
||||
};
|
||||
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.title}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
{size !== 'small' && (
|
||||
<span className={'ant-upload-list-item-actions'}>
|
||||
<Space size={3}>
|
||||
<Button
|
||||
size={'small'}
|
||||
type={'text'}
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={async () => {
|
||||
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
|
||||
}
|
||||
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={() => {
|
||||
const file = images[photoIndex];
|
||||
saveAs(file.url, `${file.title}${file.extname}`);
|
||||
}}
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
export default Upload;
|
||||
|
53
packages/client/src/schemas/upload/style.less
Normal file
53
packages/client/src/schemas/upload/style.less
Normal file
@ -0,0 +1,53 @@
|
||||
.nb-file-list {
|
||||
display: flex;
|
||||
}
|
||||
.nb-file-item {
|
||||
}
|
||||
|
||||
.nb-upload {
|
||||
.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,.5);
|
||||
// .anticon {
|
||||
// color: #1890ff;
|
||||
// }
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
10
yarn.lock
10
yarn.lock
@ -3610,6 +3610,11 @@
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/file-saver@^2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.3.tgz#b734c4f5a04d20615eaed3dc106e2ab321082009"
|
||||
integrity sha512-MBIou8pd/41jkff7s97B47bc9+p0BszqqDJsO51yDm49uUxeKzrfuNl5fSLC6BpLEWKA8zlwyqALVmXrFwoBHQ==
|
||||
|
||||
"@types/glob@^7.1.1":
|
||||
version "7.1.3"
|
||||
resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
|
||||
@ -8719,6 +8724,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.npmjs.org/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
|
||||
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
|
Loading…
Reference in New Issue
Block a user