itcloud/frontend/src/components/UploadDialog.tsx

258 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
Typography,
LinearProgress,
List,
ListItem,
ListItemText,
IconButton,
Alert,
} from '@mui/material';
import {
CloudUpload as UploadIcon,
Close as CloseIcon,
CheckCircle as SuccessIcon,
Error as ErrorIcon,
} from '@mui/icons-material';
import api from '../services/api';
interface UploadFile {
file: File;
progress: number;
status: 'pending' | 'uploading' | 'success' | 'error';
error?: string;
assetId?: string;
}
interface UploadDialogProps {
open: boolean;
onClose: () => void;
onComplete?: () => void;
}
export default function UploadDialog({ open, onClose, onComplete }: UploadDialogProps) {
const [files, setFiles] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const onDrop = useCallback((acceptedFiles: File[]) => {
const newFiles: UploadFile[] = acceptedFiles.map((file) => ({
file,
progress: 0,
status: 'pending',
}));
setFiles((prev) => [...prev, ...newFiles]);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
'video/*': ['.mp4', '.mov', '.avi', '.mkv', '.webm'],
},
});
const updateFileProgress = (index: number, progress: number, status: UploadFile['status'], error?: string, assetId?: string) => {
setFiles((prev) =>
prev.map((f, i) =>
i === index ? { ...f, progress, status, error, assetId } : f
)
);
};
const uploadFile = async (file: File, index: number) => {
try {
updateFileProgress(index, 0, 'uploading');
// Step 1: Create upload
const uploadData = await api.createUpload({
original_filename: file.name,
content_type: file.type,
size_bytes: file.size,
});
updateFileProgress(index, 33, 'uploading', undefined, uploadData.asset_id);
// Step 2: Upload file to backend
await api.uploadFileToBackend(uploadData.asset_id, file);
updateFileProgress(index, 66, 'uploading', undefined, uploadData.asset_id);
// Step 3: Finalize upload
await api.finalizeUpload(uploadData.asset_id);
updateFileProgress(index, 100, 'success', undefined, uploadData.asset_id);
} catch (error: any) {
console.error('Upload failed:', error);
updateFileProgress(
index,
0,
'error',
error.response?.data?.detail || 'Ошибка загрузки'
);
}
};
const handleUpload = async () => {
setUploading(true);
// Upload files in parallel (max 3 at a time)
const batchSize = 3;
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
await Promise.all(
batch.map((file, batchIndex) => {
const fileIndex = i + batchIndex;
if (files[fileIndex].status === 'pending') {
return uploadFile(files[fileIndex].file, fileIndex);
}
return Promise.resolve();
})
);
}
setUploading(false);
if (onComplete) {
onComplete();
}
};
const handleClose = () => {
if (!uploading) {
setFiles([]);
onClose();
}
};
const canUpload = files.length > 0 && files.some((f) => f.status === 'pending');
const allComplete = files.length > 0 && files.every((f) => f.status === 'success' || f.status === 'error');
return (
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogTitle>
Загрузка файлов
<IconButton
onClick={handleClose}
disabled={uploading}
sx={{ position: 'absolute', right: 8, top: 8 }}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
{files.length === 0 && (
<Box
{...getRootProps()}
sx={{
border: '2px dashed',
borderColor: isDragActive ? 'primary.main' : 'grey.400',
borderRadius: 2,
p: 4,
textAlign: 'center',
cursor: 'pointer',
bgcolor: isDragActive ? 'action.hover' : 'transparent',
transition: 'all 0.3s',
'&:hover': {
bgcolor: 'action.hover',
borderColor: 'primary.main',
},
}}
>
<input {...getInputProps()} />
<UploadIcon sx={{ fontSize: 64, color: 'primary.main', mb: 2 }} />
<Typography variant="h6" gutterBottom>
{isDragActive
? 'Отпустите файлы для загрузки'
: 'Перетащите файлы сюда'}
</Typography>
<Typography variant="body2" color="text.secondary">
или нажмите для выбора файлов
</Typography>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 2 }}>
Поддерживаются фото (JPG, PNG, GIF, WebP) и видео (MP4, MOV, AVI, MKV, WebM)
</Typography>
</Box>
)}
{files.length > 0 && (
<List sx={{ maxHeight: 400, overflow: 'auto' }}>
{files.map((uploadFile, index) => (
<ListItem key={index} sx={{ flexDirection: 'column', alignItems: 'stretch' }}>
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%', mb: 1 }}>
<ListItemText
primary={uploadFile.file.name}
secondary={`${(uploadFile.file.size / 1024 / 1024).toFixed(2)} MB`}
/>
{uploadFile.status === 'success' && (
<SuccessIcon color="success" />
)}
{uploadFile.status === 'error' && (
<ErrorIcon color="error" />
)}
</Box>
{uploadFile.status === 'uploading' && (
<LinearProgress
variant="determinate"
value={uploadFile.progress}
sx={{ mb: 1 }}
/>
)}
{uploadFile.error && (
<Alert severity="error" sx={{ mt: 1 }}>
{uploadFile.error}
</Alert>
)}
</ListItem>
))}
</List>
)}
{files.length > 0 && !allComplete && (
<Box
{...getRootProps()}
sx={{
mt: 2,
p: 2,
border: '1px dashed',
borderColor: 'grey.400',
borderRadius: 1,
textAlign: 'center',
cursor: 'pointer',
'&:hover': { bgcolor: 'action.hover' },
}}
>
<input {...getInputProps()} />
<Typography variant="body2" color="text.secondary">
Добавить еще файлы
</Typography>
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={uploading}>
{allComplete ? 'Закрыть' : 'Отмена'}
</Button>
{canUpload && (
<Button
onClick={handleUpload}
variant="contained"
disabled={uploading}
>
Загрузить ({files.filter((f) => f.status === 'pending').length})
</Button>
)}
</DialogActions>
</Dialog>
);
}