Bỏ qua để đến nội dung chính

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`;
}