17.02.25

Кастомизация интерфейса загрузки файлов в Ant Design Upload

10 мин · Обучающие
Кастомизация интерфейса загрузки файлов в Ant Design Upload


Введение


Компонент Upload из библиотеки Ant Design — это мощный инструмент для загрузки файлов в веб-приложения. Однако его стандартное отображение списка файлов и функционал могут не всегда соответствовать вашим задачам. В этой статье мы разберём, как создать кастомный интерфейс загрузки файлов с поддержкой двух режимов: кнопки и drag-and-drop. Также мы добавим возможность вставки файлов из буфера обмена.




Зачем нужна кастомизация?


Стандартный компонент Upload предоставляет базовые функции:

  • Загрузка файлов через кнопку.
  • Отображение списка загруженных файлов.
  • Кнопки скачивания и удаления.

Однако часто этого недостаточно. Вот несколько примеров, когда вам может понадобиться кастомизация:


  1. Режим drag-and-drop: Позволяет пользователям перетаскивать файлы в область загрузки.
  2. Вставка из буфера обмена: Удобно для работы с изображениями или другими файлами, скопированными из других приложений.
  3. Кастомный список файлов: Добавление дополнительной информации (например, дата загрузки, автор).


Мы реализуем все эти функции в одном компоненте.



Создание компонента 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.
  • Вставку файлов из буфера обмена.
  • Кастомный список загруженных файлов с дополнительной информацией.


Спасибо, в следующей статье добавим предпросмотр изображений! Удачного кодинга!


Ant Design


Design Design


Ant Design