Кастомизация интерфейса загрузки файлов в Ant Design Upload
Введение
Компонент Upload
из библиотеки Ant Design — это мощный инструмент для загрузки файлов в веб-приложения. Однако его стандартное отображение списка файлов и функционал могут не всегда соответствовать вашим задачам. В этой статье мы разберём, как создать кастомный интерфейс загрузки файлов с поддержкой двух режимов: кнопки и drag-and-drop. Также мы добавим возможность вставки файлов из буфера обмена.
Зачем нужна кастомизация?
Стандартный компонент Upload
предоставляет базовые функции:
- Загрузка файлов через кнопку.
- Отображение списка загруженных файлов.
- Кнопки скачивания и удаления.
Однако часто этого недостаточно. Вот несколько примеров, когда вам может понадобиться кастомизация:
- Режим drag-and-drop: Позволяет пользователям перетаскивать файлы в область загрузки.
- Вставка из буфера обмена: Удобно для работы с изображениями или другими файлами, скопированными из других приложений.
- Кастомный список файлов: Добавление дополнительной информации (например, дата загрузки, автор).
Мы реализуем все эти функции в одном компоненте.
Создание компонента UiUploadDraggerField
Наш компонент будет выполнять следующие задачи:
- Поддерживать два режима загрузки: кнопка и drag-and-drop.
- Отображать список загруженных файлов с дополнительной информацией.
- Предоставлять возможность скачивать и удалять файлы.
- Поддерживать вставку файлов из буфера обмена.
Код компонента UiUploadDraggerField.tsx
import { Button, message, Typography, Upload } from "antd";
import { deleteEntity, ServiceUrlEnum } from "../../services/crud-service";
import { Attachment } from "../../models/Attachment";
import React, { useEffect, useRef, useState } from "react";
import { useAuth } from "../../contexts/auth-context";
import {
downloadAttachment,
getAllAttachmentByEntityTypeAndIdAndFileType, uploadAttachment
} from "../../services/attachment-service";
import { DeleteBinIcon, UploadIcon } from "../../content/icons/SvgIcons";
import { UploadFile } from "antd/lib";
import { downloadFile } from "../../utils/fileUtil";
import dayjs from "dayjs";
import './UiUploadDraggerField.scss';
import { usePasteHandler } from "../../hooks/usePasteHandler";
import { UploadListProps, UploadListType } from "antd/es/upload/interface";
export default function UiUploadDraggerField({
entityType,
entityId,
fileType,
title = 'Выбрать файл',
listType = 'picture',
classname = '',
disabled = false,
isRemovable = true,
multiple = false,
viewMode = 'button', // new viewMode prop: 'button' or 'drag-and-drop'
inputAttachments = null,
isEmbedded = false,
onChange,
}: {
entityType: any,
entityId?: any,
fileType: string,
title?: string,
listType?: UploadListType,
classname?: string,
disabled?: boolean,
isRemovable?: boolean,
multiple?: boolean,
viewMode?: 'button' | 'drag-and-drop', // controls the display style
inputAttachments?: Attachment[] | null,
isEmbedded?: boolean,
onChange?: (attachments: Attachment[]) => void
}) {
const { token, userInfo } = useAuth();
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [loaded, setLoaded] = useState(false);
const [isEmpty, setIsEmpty] = useState(true);
const uploadAreaRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (isEmbedded) {
setAttachments(inputAttachments || []);
setLoaded(true);
return;
}
getAllAttachmentByEntityTypeAndIdAndFileType(entityType, entityId, fileType).then((res) => {
if (res?.data) {
setAttachments(res.data);
}
setLoaded(true);
});
}, [entityId, entityType, fileType, inputAttachments, isEmbedded]);
useEffect(() => {
const fileList = attachments.map((item) => ({
response: item,
name: `${item.fileName} ${item?.createdBy ? 'Загружено: ' + item?.createdBy : ''} ${item?.createdTime ?
dayjs(item.createdTime).format('DD.MM.YYYY HH:mm') : ''}`,
uid: item.id,
fileName: item.fileName,
status: 'done',
type: item.contentType,
} as UploadFile));
setFileList(fileList);
setIsEmpty(fileList.length === 0);
onChange?.(attachments);
}, [attachments]);
const handleFilePasted = (file: any) => {
uploadAttachment(entityType, entityId, fileType, { files: [file] }).then((res) => {
setAttachments((prev) => [...prev, res.data]);
});
};
const { isHovered } = usePasteHandler({
onFilePasted: handleFilePasted,
containerRef: uploadAreaRef,
});
const UploadListProps: UploadListProps = {
items: fileList,
locale: {},
showDownloadIcon: true,
showRemoveIcon: !disabled && isRemovable,
removeIcon: <DeleteBinIcon />,
listType: listType,
onRemove(file: UploadFile) {
deleteEntity<Attachment>(ServiceUrlEnum.ATTACHMENT, Attachment.Instance(file.response)).then((res) => {
if (res && res.status === 200) {
message.success(`${file.name} Успешно удален`);
setAttachments((prevState) => prevState.filter((item) => item.id !== file.uid));
}
});
},
onDownload(file: UploadFile) {
downloadAttachment(file.response.id).then((res) => {
if (res && res.data) {
downloadFile(file.fileName!, res.data);
}
});
},
};
const DraggerListProps = {
showUploadList: false,
name: 'files',
action: `${ServiceUrlEnum.ATTACHMENT}/save/${entityType}/${entityId}/${fileType}`,
headers: { Authorization: `Bearer ${token}` },
multiple: multiple,
onChange({ file, fileList, event }: { file: UploadFile, fileList: UploadFile[], event: any }) {
const status = file.status;
if (status === 'done') {
setAttachments((prevState) => [...prevState, file.response]);
message.success(`${file.name} Успешно загружен`);
}
if (status === 'error') message.error(`${file.name} Ошибка при загрузке файла.`);
},
};
return (
<>
<Typography.Title level={5}> {title} </Typography.Title>
{loaded && (
<>
{viewMode === 'drag-and-drop' ? (
<>
<div ref={uploadAreaRef}
className={`upload-area ${isHovered ? "upload-area-hovered" : ""}`}>
<Upload.Dragger className={classname} {...DraggerListProps}>
<p className="ant-upload-text">Перетащите сюда файлы или вставьте из буфера обмена</p>
</Upload.Dragger>
</div>
<Upload.List className={classname} {...UploadListProps} />
</>
) : (
<>
<Upload className={classname} {...DraggerListProps}>
{((isEmpty || multiple) && !disabled) && (
<Button className={'upload-button'} type={"text"} icon={<UploadIcon />}>
{title}
</Button>
)}
</Upload>
<Upload.List className={classname} {...UploadListProps} />
</>
)}
</>
)}
</>
);
}
Как это работает?
1. Режим drag-and-drop
- Пользователь может перетащить файлы в специальную область (
Dragger
). - При наведении курсора на эту область она подсвечивается (класс
upload-area-hovered
).
2. Режим кнопки
- Пользователь нажимает на кнопку "Выбрать файл" и выбирает файлы через диалоговое окно операционной системы.
3. Вставка из буфера обмена
- Мы добавили хук
usePasteHandler
, который позволяет вставлять файлы из буфера обмена прямо в область загрузки. - Это особенно полезно для пользователей, которые работают с изображениями или другими файлами, скопированными из других приложений.
Хук usePasteHandler
Для поддержки вставки файлов из буфера обмена мы создали хук usePasteHandler
. Вот его код:
import React, { useEffect, useState } from "react";
import { message } from "antd";
interface UsePasteHandlerProps {
onFilePasted: (file: any) => void;
containerRef: React.RefObject<HTMLDivElement>;
}
export function usePasteHandler({ onFilePasted, containerRef }: UsePasteHandlerProps) {
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
const handlePaste = (event: ClipboardEvent) => {
if (!isHovered) return;
const items = event.clipboardData?.items;
if (items) {
const itemsArray = Array.from(items);
itemsArray.forEach((item) => {
if (item.kind === "file") {
const file = item.getAsFile();
if (file) {
onFilePasted(file);
message.success(`${file.name} успешно добавлен из буфера обмена.`);
}
}
});
}
};
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
};
const container = containerRef.current;
if (container) {
container.addEventListener("mouseenter", handleMouseEnter);
container.addEventListener("mouseleave", handleMouseLeave);
}
document.addEventListener("paste", handlePaste);
return () => {
document.removeEventListener("paste", handlePaste);
if (container) {
container.removeEventListener("mouseenter", handleMouseEnter);
container.removeEventListener("mouseleave", handleMouseLeave);
}
};
}, [containerRef, onFilePasted, isHovered]);
return { isHovered };
}
Итог
Мы создали гибкий компонент UiUploadDraggerField
, который поддерживает:
- Загрузку файлов через кнопку и drag-and-drop.
- Вставку файлов из буфера обмена.
- Кастомный список загруженных файлов с дополнительной информацией.
Спасибо, в следующей статье добавим предпросмотр изображений! Удачного кодинга!
другие статьи
Смотреть всёСоздание современной PWA: Опыт Callisto
Как мы создали PWA-приложение Callisto с Workbox, Webpack и Service Worker — разбор ключевых решений и технологий.
Видеокурс Camunda 8: Создаем первый процесс
Видеокурс Camunda 8: User Tasks & Camunda Forms
Статья описывает, как управлять задачами пользователей в Camunda 8 или как сделать людей участниками исполняемого бизнес-процесса.