Tiến trình tải lên
Dropup cung cấp theo dõi tiến trình chi tiết cho tất cả các tải lên.
Theo dõi tiến trình
Tiến trình tệp riêng lẻ
Mỗi tệp có thuộc tính progress (0-100):
const { files } = useDropup({
upload: { url: '/api/upload' },
});
{files.map(file => (
<div key={file.id}>
<span>{file.name}</span>
{file.status === 'uploading' && (
<progress value={file.progress} max={100} />
)}
</div>
))}
Tiến trình tổng thể
Theo dõi tiến trình trên tất cả các tệp:
const { state } = useDropup();
// state.progress - Tiến trình trung bình của tất cả tệp (0-100)
// state.isUploading - Liệu có tải lên nào đang diễn ra
<div>
{state.isUploading && (
<p>Đang tải lên: {state.progress}%</p>
)}
</div>
Callbacks tiến trình
Tiến trình theo tệp
useDropup({
upload: { url: '/api/upload' },
onUploadProgress: (file, progress) => {
console.log(`${file.name}: ${progress}%`);
},
});
Sự kiện tải lên
useDropup({
upload: { url: '/api/upload' },
onUploadStart: (file) => {
console.log(`Bắt đầu: ${file.name}`);
},
onUploadProgress: (file, progress) => {
console.log(`Tiến trình: ${file.name} - ${progress}%`);
},
onUploadComplete: (file, response) => {
console.log(`Hoàn thành: ${file.name}`);
console.log(`URL: ${file.uploadedUrl}`);
},
onUploadError: (file, error) => {
console.error(`Thất bại: ${file.name}`, error);
},
onAllComplete: (files) => {
console.log(`Tất cả xong! ${files.length} tệp đã tải lên`);
},
});
Xây dựng UI tiến trình
Thanh tiến trình đơn giản
function ProgressBar({ value }: { value: number }) {
return (
<div style={{
width: '100%',
height: 8,
backgroundColor: '#e0e0e0',
borderRadius: 4,
}}>
<div style={{
width: `${value}%`,
height: '100%',
backgroundColor: '#4caf50',
borderRadius: 4,
transition: 'width 0.2s',
}} />
</div>
);
}
// Sử dụng
{file.status === 'uploading' && (
<ProgressBar value={file.progress} />
)}
Tiến trình hình tròn
function CircularProgress({ value, size = 40 }: { value: number; size?: number }) {
const strokeWidth = 4;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const offset = circumference - (value / 100) * circumference;
return (
<svg width={size} height={size}>
<circle
stroke="#e0e0e0"
fill="transparent"
strokeWidth={strokeWidth}
r={radius}
cx={size / 2}
cy={size / 2}
/>
<circle
stroke="#4caf50"
fill="transparent"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
r={radius}
cx={size / 2}
cy={size / 2}
style={{
transform: 'rotate(-90deg)',
transformOrigin: '50% 50%',
transition: 'stroke-dashoffset 0.2s',
}}
/>
<text
x="50%"
y="50%"
textAnchor="middle"
dy=".3em"
fontSize={size / 4}
>
{value}%
</text>
</svg>
);
}
Thẻ tải lên tệp hoàn chỉnh
function FileUploadCard({ file }: { file: DropupFile }) {
const getStatusIcon = () => {
switch (file.status) {
case 'idle': return '⏳';
case 'uploading': return '📤';
case 'complete': return '✅';
case 'error': return '❌';
case 'paused': return '⏸️';
default: return '📄';
}
};
const getStatusColor = () => {
switch (file.status) {
case 'uploading': return '#2196f3';
case 'complete': return '#4caf50';
case 'error': return '#f44336';
default: return '#9e9e9e';
}
};
return (
<div style={{
display: 'flex',
alignItems: 'center',
padding: 12,
border: '1px solid #e0e0e0',
borderRadius: 8,
marginBottom: 8,
}}>
{/* Xem trước hoặc biểu tượng */}
{file.preview ? (
<img
src={file.preview}
alt=""
style={{ width: 48, height: 48, objectFit: 'cover', borderRadius: 4 }}
/>
) : (
<span style={{ fontSize: 32 }}>{getStatusIcon()}</span>
)}
{/* Thông tin tệp */}
<div style={{ flex: 1, marginLeft: 12 }}>
<div style={{ fontWeight: 500 }}>{file.name}</div>
<div style={{ fontSize: 12, color: '#666' }}>
{(file.size / 1024).toFixed(1)} KB
</div>
{/* Thanh tiến trình */}
{file.status === 'uploading' && (
<div style={{
marginTop: 4,
height: 4,
backgroundColor: '#e0e0e0',
borderRadius: 2,
}}>
<div style={{
width: `${file.progress}%`,
height: '100%',
backgroundColor: getStatusColor(),
borderRadius: 2,
}} />
</div>
)}
{/* Thông báo lỗi */}
{file.error && (
<div style={{ color: '#f44336', fontSize: 12, marginTop: 4 }}>
{file.error.message}
</div>
)}
</div>
{/* Badge trạng thái */}
<span style={{
padding: '4px 8px',
borderRadius: 4,
fontSize: 12,
color: 'white',
backgroundColor: getStatusColor(),
}}>
{file.status === 'uploading' ? `${file.progress}%` : file.status}
</span>
</div>
);
}
Đếm trạng thái
Theo dõi số lượng theo trạng thái tải lên:
const { files, state } = useDropup();
const counts = {
total: files.length,
pending: files.filter(f => f.status === 'idle').length,
uploading: files.filter(f => f.status === 'uploading').length,
complete: files.filter(f => f.status === 'complete').length,
failed: files.filter(f => f.status === 'error').length,
};
// Hiển thị
<div>
<p>Tổng: {counts.total}</p>
<p>Đang tải: {counts.uploading}</p>
<p>Hoàn thành: {counts.complete}</p>
<p>Thất bại: {counts.failed}</p>
</div>
Tốc độ tải lên & ETA
Tính tốc độ tải lên và thời gian ước tính:
import { useState, useRef, useEffect } from 'react';
function useUploadSpeed(file: DropupFile) {
const [speed, setSpeed] = useState(0);
const [eta, setEta] = useState<number | null>(null);
const lastProgress = useRef(0);
const lastTime = useRef(Date.now());
useEffect(() => {
if (file.status !== 'uploading') return;
const interval = setInterval(() => {
const now = Date.now();
const elapsed = (now - lastTime.current) / 1000;
const progressDiff = file.progress - lastProgress.current;
const bytesDiff = (progressDiff / 100) * file.size;
if (elapsed > 0) {
const currentSpeed = bytesDiff / elapsed;
setSpeed(currentSpeed);
const remaining = file.size * (1 - file.progress / 100);
if (currentSpeed > 0) {
setEta(remaining / currentSpeed);
}
}
lastProgress.current = file.progress;
lastTime.current = now;
}, 1000);
return () => clearInterval(interval);
}, [file.status, file.progress, file.size]);
return { speed, eta };
}
// Sử dụng
function FileProgress({ file }: { file: DropupFile }) {
const { speed, eta } = useUploadSpeed(file);
return (
<div>
<p>Tốc độ: {formatSpeed(speed)}</p>
<p>Thời gian còn lại: {formatTime(eta)}</p>
</div>
);
}
function formatSpeed(bytesPerSecond: number): string {
if (bytesPerSecond < 1024) return `${bytesPerSecond.toFixed(0)} B/s`;
if (bytesPerSecond < 1024 * 1024) return `${(bytesPerSecond / 1024).toFixed(1)} KB/s`;
return `${(bytesPerSecond / 1024 / 1024).toFixed(1)} MB/s`;
}
function formatTime(seconds: number | null): string {
if (!seconds) return '--';
if (seconds < 60) return `${Math.ceil(seconds)}s`;
return `${Math.ceil(seconds / 60)}m`;
}