improve upload component

This commit is contained in:
chenos 2021-08-14 23:48:29 +08:00
parent 15105c1efb
commit a635c955c6
7 changed files with 453 additions and 21 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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,
},
},
},
};

View File

@ -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',
},
},
},
};

View File

@ -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;

View 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;
}
}

View File

@ -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"