feat: 添加文件传输速度和剩余时间计算,优化文件传输进度显示

This commit is contained in:
MatrixSeven
2025-09-10 16:49:18 +08:00
parent 84d7caea8c
commit 4b31e76488
4 changed files with 416 additions and 16 deletions

View File

@@ -1,11 +1,12 @@
"use client";
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Download, FileText, Image, Video, Music, Archive } from 'lucide-react';
import { Download, FileText, Image, Video, Music, Archive, Clock, Zap } from 'lucide-react';
import { useToast } from '@/components/ui/toast-simple';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import { TransferProgressTracker, formatTransferSpeed, formatTime } from '@/lib/transfer-utils';
interface FileInfo {
id: string;
@@ -14,6 +15,8 @@ interface FileInfo {
type: string;
status: 'ready' | 'downloading' | 'completed';
progress: number;
transferSpeed?: number; // bytes per second
startTime?: number; // 传输开始时间
}
const getFileIcon = (mimeType: string) => {
@@ -60,6 +63,9 @@ export function WebRTCFileReceive({
const [pickupCode, setPickupCode] = useState('');
const [isValidating, setIsValidating] = useState(false);
const { showToast } = useToast();
// 用于跟踪传输进度的trackers
const transferTrackers = useRef<Map<string, TransferProgressTracker>>(new Map());
// 使用传入的取件码或本地状态的取件码
const displayPickupCode = propPickupCode || pickupCode;
@@ -242,16 +248,44 @@ export function WebRTCFileReceive({
const isDownloading = file.status === 'downloading';
const isCompleted = file.status === 'completed';
const hasDownloadedFile = downloadedFiles?.has(file.id);
const currentProgress = file.progress;
console.log('文件状态:', {
fileName: file.name,
status: file.status,
progress: file.progress,
isDownloading,
currentProgress
isDownloading
});
// 计算传输进度信息
let transferInfo = null;
let currentProgress = 0; // 使用稳定的进度值
if (isDownloading && file) {
const fileKey = `${file.name}-${file.size}`;
let tracker = transferTrackers.current.get(fileKey);
// 如果tracker不存在创建一个新的
if (!tracker) {
tracker = new TransferProgressTracker(file.size);
transferTrackers.current.set(fileKey, tracker);
}
// 更新传输进度
const transferredBytes = (file.progress / 100) * file.size;
const progressInfo = tracker.update(transferredBytes);
transferInfo = progressInfo;
currentProgress = progressInfo.percentage; // 使用稳定的百分比
} else {
// 如果不在传输中,使用原始进度值
currentProgress = file.progress;
}
// 清理已完成的tracker
if (file.status === 'completed') {
const fileKey = `${file.name}-${file.size}`;
transferTrackers.current.delete(fileKey);
}
return (
<div key={file.id} className="bg-gradient-to-r from-slate-50 to-blue-50 border border-slate-200 rounded-xl p-3 sm:p-4 hover:shadow-md transition-all duration-200">
<div className="flex flex-col sm:flex-row sm:items-center justify-between mb-3 gap-3">
@@ -266,7 +300,27 @@ export function WebRTCFileReceive({
<p className="text-xs text-emerald-600 font-medium"> </p>
)}
{isDownloading && (
<p className="text-xs text-blue-600 font-medium"> ...{currentProgress.toFixed(1)}%</p>
<div className="space-y-1">
{/* 传输速度和剩余时间信息 */}
{transferInfo && (
<div className="flex items-center space-x-3">
<div className="flex items-center gap-1 text-xs text-blue-600">
<Zap className="w-3 h-3 flex-shrink-0" />
<span className="w-3 font-mono text-right">{transferInfo.speed.displaySpeed}</span>
<span className='w-2'/>
<span className="w-3">{transferInfo.speed.unit}</span>
<span className='w-3'/>
</div>
{transferInfo.remainingTime.seconds < Infinity && (
<div className="flex items-center gap-1 text-xs text-slate-600">
<Clock className="w-3 h-3 flex-shrink-0" />
<span></span>
<span className="w-3 font-mono text-right">{transferInfo.remainingTime.display}</span>
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
@@ -289,7 +343,9 @@ export function WebRTCFileReceive({
{(isDownloading || isCompleted) && currentProgress > 0 && (
<div className="mt-3 space-y-2">
<div className="flex justify-between text-sm text-slate-600">
<span>{hasDownloadedFile ? '传输完成' : '正在传输...'}</span>
<span>
{hasDownloadedFile ? '传输完成' : '正在传输...'}
</span>
<span className="font-medium">{currentProgress.toFixed(1)}%</span>
</div>
<div className="w-full bg-slate-200 rounded-full h-2">

View File

@@ -1,10 +1,11 @@
"use client";
import React, { useState, useRef, useCallback } from 'react';
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Upload, FileText, Image, Video, Music, Archive, X } from 'lucide-react';
import { Upload, FileText, Image, Video, Music, Archive, X, Clock, Zap } from 'lucide-react';
import RoomInfoDisplay from '@/components/RoomInfoDisplay';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import { TransferProgressTracker, formatTransferSpeed, formatTime } from '@/lib/transfer-utils';
interface FileInfo {
@@ -14,6 +15,8 @@ interface FileInfo {
type: string;
status: 'ready' | 'downloading' | 'completed';
progress: number;
transferSpeed?: number; // bytes per second
startTime?: number; // 传输开始时间
}
const getFileIcon = (mimeType: string) => {
@@ -65,6 +68,9 @@ export function WebRTCFileUpload({
}: WebRTCFileUploadProps) {
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 用于跟踪传输进度的trackers
const transferTrackers = useRef<Map<string, TransferProgressTracker>>(new Map());
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
@@ -197,9 +203,38 @@ export function WebRTCFileUpload({
// 查找对应的文件信息(包含状态和进度)
const fileInfo = fileList.find(f => f.name === file.name && f.size === file.size);
const isTransferringThisFile = fileInfo?.status === 'downloading';
const currentProgress = fileInfo?.progress || 0;
const fileStatus = fileInfo?.status || 'ready';
// 计算传输进度信息
let transferInfo = null;
let currentProgress = 0; // 使用稳定的进度值
if (isTransferringThisFile && fileInfo) {
const fileKey = `${file.name}-${file.size}`;
let tracker = transferTrackers.current.get(fileKey);
// 如果tracker不存在创建一个新的
if (!tracker) {
tracker = new TransferProgressTracker(file.size);
transferTrackers.current.set(fileKey, tracker);
}
// 更新传输进度
const transferredBytes = (fileInfo.progress / 100) * file.size;
const progressInfo = tracker.update(transferredBytes);
transferInfo = progressInfo;
currentProgress = progressInfo.percentage; // 使用稳定的百分比
} else {
// 如果不在传输中,使用原始进度值
currentProgress = fileInfo?.progress || 0;
}
// 清理已完成的tracker
if (fileStatus === 'completed') {
const fileKey = `${file.name}-${file.size}`;
transferTrackers.current.delete(fileKey);
}
return (
<div
key={`${file.name}-${file.size}-${index}`}
@@ -227,6 +262,26 @@ export function WebRTCFileUpload({
</div>
)}
</div>
{/* 传输速度和剩余时间信息 */}
{transferInfo && (
<div className="flex items-center space-x-3 mt-1">
<div className="flex items-center gap-1 text-xs text-blue-600">
<Zap className="w-3 h-3 flex-shrink-0" />
<span className="w-3 font-mono text-right">{transferInfo.speed.displaySpeed}</span>
<span className='w-2'/>
<span className="w-3">{transferInfo.speed.unit}</span>
<span className='w-3'/>
</div>
{transferInfo.remainingTime.seconds < Infinity && (
<div className="flex items-center gap-1 text-xs text-slate-600">
<Clock className="w-3 h-3 flex-shrink-0" />
<span></span>
<span className="w-3 font-mono text-right">{transferInfo.remainingTime.display}</span>
</div>
)}
</div>
)}
</div>
</div>
<Button
@@ -245,7 +300,9 @@ export function WebRTCFileUpload({
<div className="px-3 sm:px-4 pb-3 sm:pb-4">
<div className="space-y-2">
<div className="flex justify-between text-xs text-slate-600">
<span>{fileStatus === 'downloading' ? '正在发送...' : '发送完成'}</span>
<span>
{fileStatus === 'downloading' ? '正在发送...' : '发送完成'}
</span>
<span className="font-medium">{currentProgress.toFixed(1)}%</span>
</div>
<div className="w-full bg-slate-200 rounded-full h-2">

View File

@@ -7,6 +7,8 @@ interface FileInfo {
type: string;
status: 'ready' | 'downloading' | 'completed';
progress: number;
transferSpeed?: number; // bytes per second
startTime?: number; // 传输开始时间
}
interface UseFileStateManagerProps {
@@ -72,21 +74,33 @@ export const useFileStateManager = ({
}, []);
// 更新文件状态
const updateFileStatus = useCallback((fileId: string, status: FileInfo['status'], progress?: number) => {
const updateFileStatus = useCallback((fileId: string, status: FileInfo['status'], progress?: number, transferSpeed?: number) => {
setFileList(prev => prev.map(item =>
item.id === fileId
? { ...item, status, progress: progress ?? item.progress }
? {
...item,
status,
progress: progress ?? item.progress,
transferSpeed: transferSpeed ?? item.transferSpeed,
startTime: status === 'downloading' && !item.startTime ? Date.now() : item.startTime
}
: item
));
}, []);
// 更新文件进度
const updateFileProgress = useCallback((fileId: string, fileName: string, progress: number) => {
const updateFileProgress = useCallback((fileId: string, fileName: string, progress: number, transferSpeed?: number) => {
const newStatus = progress >= 100 ? 'completed' as const : 'downloading' as const;
setFileList(prev => prev.map(item => {
if (item.id === fileId || item.name === fileName) {
console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${progress}`);
return { ...item, progress, status: newStatus };
console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${progress}${transferSpeed ? `, 速度: ${transferSpeed} B/s` : ''}`);
return {
...item,
progress,
status: newStatus,
transferSpeed: transferSpeed ?? item.transferSpeed,
startTime: newStatus === 'downloading' && !item.startTime ? Date.now() : item.startTime
};
}
return item;
}));

View File

@@ -0,0 +1,273 @@
/**
* 传输速度和时间计算工具
*/
export interface TransferSpeed {
bytesPerSecond: number;
displaySpeed: string;
unit: string;
}
export interface TransferProgress {
totalBytes: number;
transferredBytes: number;
percentage: number;
speed: TransferSpeed;
remainingTime: {
seconds: number;
display: string;
};
elapsedTime: {
seconds: number;
display: string;
};
}
/**
* 格式化传输速度显示
* @param bytesPerSecond 每秒传输的字节数
* @returns 格式化的速度显示
*/
export function formatTransferSpeed(bytesPerSecond: number): TransferSpeed {
if (bytesPerSecond < 1024) {
return {
bytesPerSecond,
displaySpeed: `${bytesPerSecond.toFixed(0)}`,
unit: 'B/s'
};
} else if (bytesPerSecond < 1024 * 1024) {
const kbps = bytesPerSecond / 1024;
return {
bytesPerSecond,
displaySpeed: `${kbps.toFixed(1)}`,
unit: 'KB/s'
};
} else {
const mbps = bytesPerSecond / (1024 * 1024);
return {
bytesPerSecond,
displaySpeed: `${mbps.toFixed(1)}`,
unit: 'MB/s'
};
}
}
/**
* 格式化时间显示
* @param seconds 秒数
* @returns 格式化的时间显示
*/
export function formatTime(seconds: number): string {
if (!isFinite(seconds) || seconds < 0) {
return '--:--';
}
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
}
/**
* 传输进度跟踪器
*/
export class TransferProgressTracker {
private startTime: number;
private lastUpdateTime: number;
private lastSpeedUpdateTime: number;
private lastProgressUpdateTime: number;
private lastTransferredBytes: number;
private speedHistory: number[] = [];
private readonly maxHistorySize = 10; // 保持最近10个速度样本
private readonly speedUpdateInterval = 300; // 速度更新间隔0.3秒
private readonly progressUpdateInterval = 50; // 进度更新间隔0.3秒
private cachedProgress: TransferProgress | null = null;
private lastDisplayedSpeed: TransferSpeed;
private lastDisplayedPercentage: number = 0;
constructor(
private totalBytes: number,
private initialTransferredBytes: number = 0
) {
this.startTime = Date.now();
this.lastUpdateTime = this.startTime;
this.lastSpeedUpdateTime = this.startTime;
this.lastProgressUpdateTime = this.startTime;
this.lastTransferredBytes = initialTransferredBytes;
this.lastDisplayedSpeed = formatTransferSpeed(0);
}
/**
* 更新传输进度
* @param transferredBytes 已传输的字节数
* @returns 传输进度信息
*/
update(transferredBytes: number): TransferProgress {
const now = Date.now();
const elapsedTimeMs = now - this.startTime;
const timeSinceLastUpdate = now - this.lastUpdateTime;
const timeSinceLastSpeedUpdate = now - this.lastSpeedUpdateTime;
const timeSinceLastProgressUpdate = now - this.lastProgressUpdateTime;
// 计算即时速度(基于最近的更新)
let instantSpeed = 0;
if (timeSinceLastUpdate > 0) {
const bytesDiff = transferredBytes - this.lastTransferredBytes;
instantSpeed = (bytesDiff * 1000) / timeSinceLastUpdate; // bytes per second
}
// 只有当距离上次速度更新超过指定间隔时才更新速度显示
let shouldUpdateSpeed = timeSinceLastSpeedUpdate >= this.speedUpdateInterval;
// 只有当距离上次进度更新超过指定间隔时才更新进度显示
let shouldUpdateProgress = timeSinceLastProgressUpdate >= this.progressUpdateInterval;
// 如果是第一次更新或者传输完成,立即更新速度和进度
if (this.cachedProgress === null || transferredBytes >= this.totalBytes) {
shouldUpdateSpeed = true;
shouldUpdateProgress = true;
}
if (shouldUpdateSpeed) {
// 更新速度历史
if (instantSpeed > 0) {
this.speedHistory.push(instantSpeed);
if (this.speedHistory.length > this.maxHistorySize) {
this.speedHistory.shift();
}
}
// 计算平均速度
let averageSpeed = 0;
if (this.speedHistory.length > 0) {
averageSpeed = this.speedHistory.reduce((sum, speed) => sum + speed, 0) / this.speedHistory.length;
} else if (elapsedTimeMs > 0) {
// 如果没有即时速度历史,使用总体平均速度
averageSpeed = (transferredBytes * 1000) / elapsedTimeMs;
}
// 更新显示的速度
this.lastDisplayedSpeed = formatTransferSpeed(averageSpeed);
this.lastSpeedUpdateTime = now;
}
// 更新显示的进度百分比
if (shouldUpdateProgress) {
const currentPercentage = this.totalBytes > 0 ? (transferredBytes / this.totalBytes) * 100 : 0;
this.lastDisplayedPercentage = Math.min(currentPercentage, 100);
this.lastProgressUpdateTime = now;
}
// 计算剩余时间(使用当前显示的速度)
const remainingBytes = this.totalBytes - transferredBytes;
const remainingTimeSeconds = this.lastDisplayedSpeed.bytesPerSecond > 0
? remainingBytes / this.lastDisplayedSpeed.bytesPerSecond
: Infinity;
// 更新跟踪状态
this.lastUpdateTime = now;
this.lastTransferredBytes = transferredBytes;
// 创建进度对象(使用稳定的进度值)
const progress: TransferProgress = {
totalBytes: this.totalBytes,
transferredBytes,
percentage: this.lastDisplayedPercentage,
speed: this.lastDisplayedSpeed,
remainingTime: {
seconds: remainingTimeSeconds,
display: formatTime(remainingTimeSeconds)
},
elapsedTime: {
seconds: elapsedTimeMs / 1000,
display: formatTime(elapsedTimeMs / 1000)
}
};
// 缓存进度信息
this.cachedProgress = progress;
return progress;
}
/**
* 重置跟踪器
*/
reset(totalBytes?: number, initialTransferredBytes: number = 0) {
if (totalBytes !== undefined) {
this.totalBytes = totalBytes;
}
this.startTime = Date.now();
this.lastUpdateTime = this.startTime;
this.lastSpeedUpdateTime = this.startTime;
this.lastProgressUpdateTime = this.startTime;
this.lastTransferredBytes = initialTransferredBytes;
this.speedHistory = [];
this.cachedProgress = null;
this.lastDisplayedSpeed = formatTransferSpeed(0);
this.lastDisplayedPercentage = 0;
}
/**
* 获取总字节数
*/
getTotalBytes(): number {
return this.totalBytes;
}
/**
* 获取平均速度(整个传输过程)
*/
getOverallAverageSpeed(): number {
const elapsedTimeMs = Date.now() - this.startTime;
if (elapsedTimeMs > 0) {
return (this.lastTransferredBytes * 1000) / elapsedTimeMs;
}
return 0;
}
}
/**
* 创建传输进度跟踪器
* @param totalBytes 总字节数
* @param initialTransferredBytes 初始已传输字节数
* @returns 传输进度跟踪器实例
*/
export function createTransferTracker(totalBytes: number, initialTransferredBytes: number = 0): TransferProgressTracker {
return new TransferProgressTracker(totalBytes, initialTransferredBytes);
}
/**
* 简单的传输速度计算(无状态)
* @param transferredBytes 已传输字节数
* @param elapsedTimeMs 经过的时间(毫秒)
* @returns 格式化的速度
*/
export function calculateSpeed(transferredBytes: number, elapsedTimeMs: number): TransferSpeed {
if (elapsedTimeMs <= 0) {
return formatTransferSpeed(0);
}
const bytesPerSecond = (transferredBytes * 1000) / elapsedTimeMs;
return formatTransferSpeed(bytesPerSecond);
}
/**
* 计算剩余时间
* @param remainingBytes 剩余字节数
* @param bytesPerSecond 每秒传输字节数
* @returns 格式化的剩余时间
*/
export function calculateRemainingTime(remainingBytes: number, bytesPerSecond: number): string {
if (bytesPerSecond <= 0 || remainingBytes <= 0) {
return '--:--';
}
const remainingSeconds = remainingBytes / bytesPerSecond;
return formatTime(remainingSeconds);
}