From 2050eb5317c928ce020f772caef050481b351487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?= =?UTF-8?q?=E7=A9=BA=E6=B0=94-Rairn?= <958414905@qq.com> Date: Sun, 9 Apr 2023 16:46:53 +0800 Subject: [PATCH] feat: optimize file collection (#1666) * feat(Upload.DraggerV2): add loading * feat: auto close on uploading over * feat: auto selected when upload is complete * feat: filter selected options * feat: add quick-upload and select-file * fix: should not appear on readPretty mode * fix: radio * fix: compatible with older version * feat: improving the interaction of radio selections * fix: show two butons by default --- .../collection-manager/interfaces/linkTo.ts | 4 +- .../src/collection-manager/interfaces/m2m.tsx | 2 + .../src/collection-manager/interfaces/m2o.tsx | 2 + .../src/collection-manager/interfaces/o2m.tsx | 8 +- .../src/collection-manager/interfaces/o2o.tsx | 18 +-- .../antd/form-item/FormItem.tsx | 49 ++++++- .../schema-component/antd/preview/Preview.tsx | 123 ++++++++++++++---- .../antd/record-picker/InputRecordPicker.tsx | 100 +++++++++++--- .../schema-component/antd/table-v2/Table.tsx | 4 +- .../schema-component/antd/upload/Upload.tsx | 20 ++- .../schema-component/antd/upload/shared.ts | 2 +- .../src/client/hooks/useUploadFiles.ts | 15 ++- .../initializers/UploadActionInitializer.tsx | 5 +- 13 files changed, 273 insertions(+), 79 deletions(-) diff --git a/packages/core/client/src/collection-manager/interfaces/linkTo.ts b/packages/core/client/src/collection-manager/interfaces/linkTo.ts index a1b0632b4..ffc06dd02 100644 --- a/packages/core/client/src/collection-manager/interfaces/linkTo.ts +++ b/packages/core/client/src/collection-manager/interfaces/linkTo.ts @@ -45,12 +45,14 @@ export const linkTo: IField = { }, }, }, - availableTypes:['belongsToMany'], + availableTypes: ['belongsToMany'], schemaInitialize(schema: ISchema, { readPretty, block, targetCollection }) { if (targetCollection?.template === 'file') { const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; fieldNames.label = 'preview'; schema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'].quickUpload = true; + schema['x-component-props'].selectFile = true; } if (block === 'Form') { diff --git a/packages/core/client/src/collection-manager/interfaces/m2m.tsx b/packages/core/client/src/collection-manager/interfaces/m2m.tsx index 231520eb7..4914d481c 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2m.tsx @@ -57,6 +57,8 @@ export const m2m: IField = { const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; fieldNames.label = 'preview'; schema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'].quickUpload = true; + schema['x-component-props'].selectFile = true; } if (block === 'Form') { diff --git a/packages/core/client/src/collection-manager/interfaces/m2o.tsx b/packages/core/client/src/collection-manager/interfaces/m2o.tsx index 26e18d348..35ad1160f 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2o.tsx @@ -56,6 +56,8 @@ export const m2o: IField = { const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; fieldNames.label = 'preview'; schema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'].quickUpload = true; + schema['x-component-props'].selectFile = true; } if (block === 'Form') { diff --git a/packages/core/client/src/collection-manager/interfaces/o2m.tsx b/packages/core/client/src/collection-manager/interfaces/o2m.tsx index df012027c..817e1d35b 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2m.tsx @@ -51,13 +51,7 @@ export const o2m: IField = { }, }, availableTypes: ['hasMany'], - schemaInitialize(schema: ISchema, { field, block, readPretty, targetCollection }) { - if (targetCollection?.template === 'file') { - const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; - fieldNames.label = 'preview'; - schema['x-component-props']['fieldNames'] = fieldNames; - } - + schemaInitialize(schema: ISchema, { field, block, readPretty }) { if (block === 'Form') { if (schema['x-component'] === 'TableField') { const association = `${field.collectionName}.${field.name}`; diff --git a/packages/core/client/src/collection-manager/interfaces/o2o.tsx b/packages/core/client/src/collection-manager/interfaces/o2o.tsx index 7945f936e..2b94dc2ce 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2o.tsx @@ -118,7 +118,7 @@ export const o2o: IField = { }, }, availableTypes: ['hasOne'], - schemaInitialize(schema: ISchema, { field, block, readPretty, action, targetCollection }) { + schemaInitialize(schema: ISchema, { field, block, readPretty, action }) { internalSchameInitialize(schema, { field, block, readPretty, action }); if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; @@ -127,12 +127,6 @@ export const o2o: IField = { // 预览文件时需要的参数 schema['x-component-props']['size'] = 'small'; } - - if (targetCollection?.template === 'file') { - const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; - fieldNames.label = 'preview'; - schema['x-component-props']['fieldNames'] = fieldNames; - } }, properties: { 'uiSchema.title': { @@ -302,7 +296,7 @@ export const oho: IField = { }, }, }, - schemaInitialize(schema: ISchema, { field, block, readPretty, action, targetCollection }) { + schemaInitialize(schema: ISchema, { field, block, readPretty, action }) { internalSchameInitialize(schema, { field, block, readPretty, action }); if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; @@ -311,12 +305,6 @@ export const oho: IField = { // 预览文件时需要的参数 schema['x-component-props']['size'] = 'small'; } - - if (targetCollection?.template === 'file') { - const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; - fieldNames.label = 'preview'; - schema['x-component-props']['fieldNames'] = fieldNames; - } }, properties: { 'uiSchema.title': { @@ -491,6 +479,8 @@ export const obo: IField = { const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; fieldNames.label = 'preview'; schema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'].quickUpload = true; + schema['x-component-props'].selectFile = true; } }, properties: { diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index b7d247535..70166d40f 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -8,7 +8,7 @@ import React, { useContext, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider'; import { BlockRequestContext, useFilterByTk, useFormBlockContext } from '../../../block-provider'; -import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { Collection, useCollection, useCollectionManager } from '../../../collection-manager'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks'; import { BlockItem } from '../block-item'; @@ -81,6 +81,7 @@ FormItem.Designer = () => { const { dn, refresh, insertAdjacent } = useDesignable(); const compile = useCompile(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const targetCollection = getCollection(collectionField.target); const interfaceConfig = getInterface(collectionField?.interface); const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); const originalTitle = collectionField?.uiSchema?.title; @@ -229,6 +230,46 @@ FormItem.Designer = () => { }} /> )} + {!form?.readPretty && isFileCollection(targetCollection) ? ( + { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.componentProps.quickUpload = value; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].quickUpload = value; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + ) : null} + {!form?.readPretty && isFileCollection(targetCollection) ? ( + { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.componentProps.selectFile = value; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].selectFile = value; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + ) : null} {form && !form?.readPretty && validateSchema && ( { block: 'Form', readPretty: field.readPretty, action: tk ? 'get' : null, - targetCollection: getCollection(collectionField.target), + targetCollection, }); insertAdjacent('beforeBegin', divWrap(schema), { @@ -547,4 +588,8 @@ FormItem.Designer = () => { ); }; +function isFileCollection(collection: Collection) { + return collection?.template === 'file'; +} + FormItem.FilterFormDesigner = FilterFormDesigner; diff --git a/packages/core/client/src/schema-component/antd/preview/Preview.tsx b/packages/core/client/src/schema-component/antd/preview/Preview.tsx index f8737d205..49c172f4a 100644 --- a/packages/core/client/src/schema-component/antd/preview/Preview.tsx +++ b/packages/core/client/src/schema-component/antd/preview/Preview.tsx @@ -1,6 +1,6 @@ import { DeleteOutlined, DownloadOutlined, PlusOutlined } from '@ant-design/icons'; import { connect, mapReadPretty } from '@formily/react'; -import { Upload as AntdUpload, Button, Space } from 'antd'; +import { Upload as AntdUpload, Button, Progress, Space } from 'antd'; import cls from 'classnames'; import { saveAs } from 'file-saver'; import React, { useEffect, useState } from 'react'; @@ -8,25 +8,34 @@ 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 '../upload/ReadPretty'; -import { isImage, toArr, toFileList } from '../upload/shared'; +import { isImage, toFileList, useUploadProps } from '../upload/shared'; import '../upload/style.less'; +import { UploadProps } from '../upload/type'; -interface TPreview { - (props: any): JSX.Element; - Selector: React.FC; -} +type Props = UploadProps & { + /** 是否显示 Upload 按钮 */ + quickUpload: boolean; + /** 是否显示 Select 按钮 */ + selectFile: boolean; + onRemove?: (file) => void; + onSelect?: () => void; +}; export const Preview = connect((props) => { return ; }, mapReadPretty(ReadPretty.File)); -export const FileSelector = (props: any) => { - const { disabled, multiple, value, onRemove, onSelect } = props; +export const FileSelector = (props: Props) => { + const { disabled, multiple, value, quickUpload, selectFile, onRemove, onSelect } = props; + const uploadProps = useUploadProps({ ...props }); const [fileList, setFileList] = useState([]); const [photoIndex, setPhotoIndex] = useState(0); const [visible, setVisible] = useState(false); const { t } = useTranslation(); + // 兼容旧版本 + const showSelectButton = selectFile === undefined && quickUpload === undefined; + useEffect(() => { setFileList(toFileList(value)); }, [value]); @@ -42,11 +51,13 @@ export const FileSelector = (props: any) => { onSelect?.(); }; + const list = fileList.length ? (multiple ? fileList : [fileList[fileList.length - 1]]) : []; + return (
- {fileList.map((file) => { + {list.map((file) => { const handleClick = (e) => { e.preventDefault(); e.stopPropagation(); @@ -59,7 +70,7 @@ export const FileSelector = (props: any) => { } }; return ( -
+
@@ -108,21 +119,25 @@ export const FileSelector = (props: any) => { )} + {file.status === 'uploading' && ( +
+ +
+ )}
); })} - {!disabled && (multiple || toArr(value).length < 1) && ( -
- - {!disabled && (multiple || toArr(value).length < 1) && ( + <> + {showSelectButton ? ( +
+
{ {t('Select')}
- )} -
-
- )} +
+
+ ) : null} + {quickUpload ? ( +
+ { + // 如果不在这里 setFileList 的话,会导致 onChange 只会执行一次 + setFileList([...info.fileList]); + uploadProps.onChange?.(info); + }} + > +
+ + {t('Upload')} +
+
+
+ ) : null} + {selectFile ? ( +
+ +
+ + {t('Select')} +
+
+
+ ) : null} +
{/* 预览图片的弹框 */} diff --git a/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx b/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx index ad830d6a1..dc1d0249a 100644 --- a/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx +++ b/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx @@ -12,7 +12,8 @@ import { FileSelector } from '../preview'; import { useFieldNames } from './useFieldNames'; import { getLabelFormatValue, useLabelUiSchema } from './util'; -const RecordPickerContext = createContext(null); +export const RecordPickerContext = createContext(null); + function flatData(data) { let newArr = []; for (let i = 0; i < data.length; i++) { @@ -27,28 +28,40 @@ function flatData(data) { const useTableSelectorProps = () => { const field = useField(); - const { multiple, value, setSelectedRows, selectedRows: rcSelectRows } = useContext(RecordPickerContext); - const { onRowSelectionChange, rowKey, ...others } = useTsp(); + const { + multiple, + options = [], + setSelectedRows, + selectedRows: rcSelectRows = [], + onChange, + } = useContext(RecordPickerContext); + const { onRowSelectionChange, rowKey = 'id', ...others } = useTsp(); + const { setVisible } = useActionContext(); + return { ...others, rowKey, rowSelection: { type: multiple ? 'checkbox' : 'radio', - // defaultSelectedRowKeys: rcSelectRows?.map((item) => item[rowKey||'id']), - selectedRowKeys: rcSelectRows?.map((item) => item[rowKey || 'id']), + selectedRowKeys: rcSelectRows + .filter((item) => options.every((row) => row[rowKey] !== item[rowKey])) + .map((item) => item[rowKey]), }, + dataSource: field.value?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey])), onRowSelectionChange(selectedRowKeys, selectedRows) { if (multiple) { const scopeRows = flatData(field.value) || []; const allSelectedRows = rcSelectRows || []; - const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey || 'id'); - const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey || 'id'); - const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey || 'id']); + const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey); + const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey); + const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey]); setSelectedRows?.(unionSelectedRows); onRowSelectionChange?.(unionSelectedRowKeys, unionSelectedRows); } else { setSelectedRows?.(selectedRows); onRowSelectionChange?.(selectedRowKeys, selectedRows); + onChange(selectedRows?.[0] || null); + setVisible(false); } }, }; @@ -56,11 +69,11 @@ const useTableSelectorProps = () => { const usePickActionProps = () => { const { setVisible } = useActionContext(); - const { multiple, selectedRows, onChange } = useContext(RecordPickerContext); + const { multiple, selectedRows, onChange, options } = useContext(RecordPickerContext); return { onClick() { if (multiple) { - onChange(selectedRows); + onChange([...options, ...selectedRows]); } else { onChange(selectedRows?.[0] || null); } @@ -79,8 +92,20 @@ const useAssociation = (props) => { return getField(fieldSchema.name); }; -export const InputRecordPicker: React.FC = (props) => { - const { value, multiple, onChange, ...others } = props; +interface IRecordPickerProps { + multiple?: boolean; + association?: string; + value?: any; + /** 是否显示 Upload 按钮(仅适用于文件场景下) */ + quickUpload?: boolean; + /** 是否显示 Select 按钮(仅适用于文件场景下) */ + selectFile?: boolean; + onChange?: (value: any) => void; + [key: string]: any; +} + +export const InputRecordPicker: React.FC = (props: IRecordPickerProps) => { + const { value, multiple, onChange, quickUpload, selectFile, ...others } = props; const fieldNames = useFieldNames(props); const [visible, setVisible] = useState(false); const fieldSchema = useFieldSchema(); @@ -101,7 +126,6 @@ export const InputRecordPicker: React.FC = (props) => { }; }); setOptions(opts); - setSelectedRows(opts); } }, [value, fieldNames?.label]); @@ -113,13 +137,12 @@ export const InputRecordPicker: React.FC = (props) => { const handleSelect = () => { setVisible(true); - setSelectedRows(options); + setSelectedRows([]); }; const handleRemove = (file) => { const newOptions = options.filter((option) => option.id !== file.id); setOptions(newOptions); - setSelectedRows(newOptions); if (newOptions.length === 0) { return onChange(null); } @@ -129,7 +152,25 @@ export const InputRecordPicker: React.FC = (props) => { return (
{showFilePicker ? ( - + { + if (changed.every((file) => file.status !== 'uploading')) { + changed = changed.filter((file) => file.status === 'done').map((file) => file.response.data); + if (multiple) { + onChange([...options, ...changed]); + } else { + onChange(changed[0]); + } + } + }} + /> ) : (