diff --git a/package.json b/package.json index 38b7b4625..36de28dfe 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/client/package.json b/packages/client/package.json index 2ff46bfda..d79503e1f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -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", diff --git a/packages/client/src/schemas/upload/demos/demo1.tsx b/packages/client/src/schemas/upload/demos/demo1.tsx index bd26170c4..695f1ce69 100644 --- a/packages/client/src/schemas/upload/demos/demo1.tsx +++ b/packages/client/src/schemas/upload/demos/demo1.tsx @@ -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, + }, }, }, }; diff --git a/packages/client/src/schemas/upload/demos/demo2.tsx b/packages/client/src/schemas/upload/demos/demo2.tsx index b124edc03..fade5091c 100644 --- a/packages/client/src/schemas/upload/demos/demo2.tsx +++ b/packages/client/src/schemas/upload/demos/demo2.tsx @@ -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', + }, + }, }, }; diff --git a/packages/client/src/schemas/upload/index.tsx b/packages/client/src/schemas/upload/index.tsx index fa0a116c9..88f6317a5 100644 --- a/packages/client/src/schemas/upload/index.tsx +++ b/packages/client/src/schemas/upload/index.tsx @@ -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 & { onChange?: (fileList: UploadFile[]) => void; serviceErrorMessage?: string; + value?: any; }; type DraggerProps = Omit & { @@ -37,6 +53,7 @@ type DraggerProps = Omit & { type ComposedUpload = React.FC & { Dragger?: React.FC; File?: React.FC; + Attachment?: React.FC; }; 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 (
{ 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 ( +
+
+
+ {fileList.map((file) => { + const handleClick = (e) => { + e.preventDefault(); + const index = fileList.indexOf(file); + setVisible(true); + setPhotoIndex(index); + }; + return ( +
+
+ + + +
+
+ ); + })} +
+ { + 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) && ( + + +
上传 +
+ )} +
+
+
+
+ {visible && ( + setVisible(false)} + onMovePrevRequest={() => + setPhotoIndex((photoIndex + images.length - 1) % images.length) + } + onMoveNextRequest={() => + setPhotoIndex((photoIndex + 1) % images.length) + } + imageTitle={images[photoIndex]?.title} + toolbarButtons={[ + , + ]} + /> + )} +
+ ); + }, + mapReadPretty((props) => { + const field = useField(); + 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 ( +
+
+
+ {images.map((file) => { + const handleClick = (e) => { + e.preventDefault(); + const index = images.indexOf(file); + setVisible(true); + setPhotoIndex(index); + }; + return ( +
+
+ + {size !== 'small' && ( + + +
+
+ ); + })} +
+
+ {visible && ( + setVisible(false)} + onMovePrevRequest={() => + setPhotoIndex((photoIndex + images.length - 1) % images.length) + } + onMoveNextRequest={() => + setPhotoIndex((photoIndex + 1) % images.length) + } + imageTitle={images[photoIndex]?.title} + toolbarButtons={[ + , + ]} + /> + )} +
+ ); + }), +); + export default Upload; diff --git a/packages/client/src/schemas/upload/style.less b/packages/client/src/schemas/upload/style.less new file mode 100644 index 000000000..e368de6c2 --- /dev/null +++ b/packages/client/src/schemas/upload/style.less @@ -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; + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index bb9a2ba56..f61355489 100644 --- a/yarn.lock +++ b/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"