mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-03-13 11:37:33 +08:00
feat:UI大调整,WEBRTC切换
This commit is contained in:
438
chuan-next/src/components/webrtc/WebRTCFileReceive.tsx
Normal file
438
chuan-next/src/components/webrtc/WebRTCFileReceive.tsx
Normal file
@@ -0,0 +1,438 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Download, FileText, Image, Video, Music, Archive } from 'lucide-react';
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
status: 'ready' | 'downloading' | 'completed';
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const getFileIcon = (mimeType: string) => {
|
||||
if (mimeType.startsWith('image/')) return <Image className="w-5 h-5 text-white" />;
|
||||
if (mimeType.startsWith('video/')) return <Video className="w-5 h-5 text-white" />;
|
||||
if (mimeType.startsWith('audio/')) return <Music className="w-5 h-5 text-white" />;
|
||||
if (mimeType.includes('zip') || mimeType.includes('rar')) return <Archive className="w-5 h-5 text-white" />;
|
||||
return <FileText className="w-5 h-5 text-white" />;
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
interface WebRTCFileReceiveProps {
|
||||
onJoinRoom: (code: string) => void;
|
||||
files: FileInfo[];
|
||||
onDownloadFile: (fileId: string) => void;
|
||||
transferProgress: number;
|
||||
isTransferring: boolean;
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
isWebSocketConnected?: boolean;
|
||||
downloadedFiles?: Map<string, File>;
|
||||
}
|
||||
|
||||
export function WebRTCFileReceive({
|
||||
onJoinRoom,
|
||||
files,
|
||||
onDownloadFile,
|
||||
transferProgress,
|
||||
isTransferring,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
isWebSocketConnected = false,
|
||||
downloadedFiles
|
||||
}: WebRTCFileReceiveProps) {
|
||||
const [pickupCode, setPickupCode] = useState('');
|
||||
|
||||
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (pickupCode.length === 6) {
|
||||
onJoinRoom(pickupCode.toUpperCase());
|
||||
}
|
||||
}, [pickupCode, onJoinRoom]);
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase();
|
||||
if (value.length <= 6) {
|
||||
setPickupCode(value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 如果已经连接但没有文件,显示等待界面
|
||||
if ((isConnected || isConnecting) && files.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
{/* 功能标题和状态 */}
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-xl flex items-center justify-center">
|
||||
<Download className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800">等待文件</h2>
|
||||
<p className="text-sm text-slate-600">
|
||||
{isConnected ? '已连接到房间,等待发送方选择文件...' : '正在连接到房间...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 竖线分割 */}
|
||||
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
||||
|
||||
{/* 状态显示 */}
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
||||
<div className="flex items-center justify-end space-x-3 text-sm">
|
||||
{/* WebSocket状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isWebSocketConnected ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-emerald-600">WS</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
|
||||
<span className="text-orange-600">WS</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分隔符 */}
|
||||
<div className="text-slate-300">|</div>
|
||||
|
||||
{/* WebRTC状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isConnected ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-emerald-600">RTC</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
|
||||
<span className="text-orange-600">RTC</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
{/* 连接状态指示器 */}
|
||||
<div className="flex items-center justify-center space-x-4 mb-6">
|
||||
<div className="flex items-center">
|
||||
<div className={`w-3 h-3 rounded-full mr-2 ${isConnected ? 'bg-emerald-500 animate-pulse' : 'bg-orange-500 animate-spin'}`}></div>
|
||||
<span className={`text-sm font-medium ${isConnected ? 'text-emerald-600' : 'text-orange-600'}`}>
|
||||
{isConnected ? '连接已建立' : '连接中...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 等待动画 */}
|
||||
<div className="flex justify-center space-x-1 mb-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
|
||||
style={{ animationDelay: `${i * 0.1}s` }}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200">
|
||||
<p className="text-xs sm:text-sm text-slate-600 text-center">
|
||||
💡 <span className="font-medium">提示:</span>房间已连接,发送方清空文件列表后您会看到此界面,等待对方重新选择文件
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果已经连接并且有文件列表,显示文件列表
|
||||
if (files.length > 0) {
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* 功能标题和状态 */}
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
|
||||
<Download className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800">可下载文件</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
{isConnected ? (
|
||||
<span className="text-emerald-600">✅ 已连接,可以下载文件</span>
|
||||
) : (
|
||||
<span className="text-amber-600">⏳ 正在建立连接...</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 竖线分割 */}
|
||||
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
||||
|
||||
{/* 状态显示 */}
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
||||
<div className="flex items-center justify-end space-x-3 text-sm">
|
||||
{/* WebSocket状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isWebSocketConnected ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-emerald-600">WS</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
||||
<span className="text-slate-600">WS</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分隔符 */}
|
||||
<div className="text-slate-300">|</div>
|
||||
|
||||
{/* WebRTC状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isConnected ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-emerald-600">RTC</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
|
||||
<span className="text-orange-600">RTC</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
{files.length} 个文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{files.map((file) => {
|
||||
const isDownloading = file.status === 'downloading';
|
||||
const isCompleted = file.status === 'completed';
|
||||
const hasDownloadedFile = downloadedFiles?.has(file.id);
|
||||
const currentProgress = isDownloading && isTransferring ? transferProgress : file.progress;
|
||||
|
||||
console.log('文件状态:', {
|
||||
fileName: file.name,
|
||||
status: file.status,
|
||||
progress: file.progress,
|
||||
isDownloading,
|
||||
isTransferring,
|
||||
transferProgress,
|
||||
currentProgress
|
||||
});
|
||||
|
||||
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">
|
||||
<div className="flex items-center space-x-3 sm:space-x-4 flex-1 min-w-0">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
{getFileIcon(file.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-slate-800 truncate text-sm sm:text-base">{file.name}</p>
|
||||
<p className="text-sm text-slate-500">{formatFileSize(file.size)}</p>
|
||||
{hasDownloadedFile && (
|
||||
<p className="text-xs text-emerald-600 font-medium">✅ 传输完成,点击保存</p>
|
||||
)}
|
||||
{isDownloading && isTransferring && (
|
||||
<p className="text-xs text-blue-600 font-medium">⏳ 传输中...{currentProgress.toFixed(1)}%</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onDownloadFile(file.id)}
|
||||
disabled={!isConnected || (isDownloading && isTransferring)}
|
||||
className={`px-6 py-2 rounded-lg font-medium shadow-lg transition-all duration-200 hover:shadow-xl ${
|
||||
hasDownloadedFile
|
||||
? 'bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 text-white'
|
||||
: (isDownloading && isTransferring)
|
||||
? 'bg-slate-300 text-slate-500 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{(isDownloading && isTransferring) ? '传输中...' : hasDownloadedFile ? '保存文件' : '开始传输'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(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 className="font-medium">{currentProgress.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
hasDownloadedFile
|
||||
? 'bg-gradient-to-r from-emerald-500 to-emerald-600'
|
||||
: 'bg-gradient-to-r from-emerald-500 to-teal-500'
|
||||
}`}
|
||||
style={{ width: `${currentProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 显示取件码输入界面
|
||||
return (
|
||||
<div>
|
||||
{/* 功能标题和状态 */}
|
||||
<div className="flex items-center mb-6 sm:mb-8">
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
|
||||
<Download className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800">输入取件码</h2>
|
||||
<p className="text-sm text-slate-600">请输入6位取件码来获取文件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 竖线分割 */}
|
||||
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
||||
|
||||
{/* 状态显示 */}
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
||||
<div className="flex items-center justify-end space-x-3 text-sm">
|
||||
{/* WebSocket状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isConnecting ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
|
||||
<span className="text-orange-600">WS</span>
|
||||
</>
|
||||
) : isWebSocketConnected ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-emerald-600">WS</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
||||
<span className="text-slate-600">WS</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分隔符 */}
|
||||
<div className="text-slate-300">|</div>
|
||||
|
||||
{/* WebRTC状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isConnected ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-emerald-600">RTC</span>
|
||||
</>
|
||||
) : isConnecting ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
|
||||
<span className="text-orange-600">RTC</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
||||
<span className="text-slate-600">RTC</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={pickupCode}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入取件码"
|
||||
className="text-center text-2xl sm:text-3xl tracking-[0.3em] sm:tracking-[0.5em] font-mono h-12 sm:h-16 border-2 border-slate-200 rounded-xl focus:border-emerald-500 focus:ring-emerald-500 bg-white/80 backdrop-blur-sm pb-2 sm:pb-4"
|
||||
maxLength={6}
|
||||
disabled={isConnecting}
|
||||
/>
|
||||
<div className="absolute inset-x-0 -bottom-4 sm:-bottom-6 flex justify-center space-x-1 sm:space-x-2">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-1.5 h-1.5 sm:w-2 sm:h-2 rounded-full transition-all duration-200 ${
|
||||
i < pickupCode.length
|
||||
? 'bg-emerald-500'
|
||||
: 'bg-slate-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-3 sm:h-4"></div>
|
||||
<p className="text-center text-xs sm:text-sm text-slate-500">
|
||||
{pickupCode.length}/6 位
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-10 sm:h-12 bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white text-base sm:text-lg font-medium rounded-xl shadow-lg transition-all duration-200 hover:shadow-xl hover:scale-105 disabled:opacity-50 disabled:scale-100"
|
||||
disabled={pickupCode.length !== 6 || isConnecting}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>连接中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Download className="w-5 h-5" />
|
||||
<span>开始接收</span>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* 使用提示 */}
|
||||
<div className="mt-6 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200">
|
||||
<p className="text-sm text-slate-600 text-center">
|
||||
💡 <span className="font-medium">提示:</span>取件码由发送方提供,有效期为24小时
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
413
chuan-next/src/components/webrtc/WebRTCFileUpload.tsx
Normal file
413
chuan-next/src/components/webrtc/WebRTCFileUpload.tsx
Normal file
@@ -0,0 +1,413 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { Upload, FileText, Image, Video, Music, Archive, X } from 'lucide-react';
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
status: 'ready' | 'downloading' | 'completed';
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const getFileIcon = (mimeType: string) => {
|
||||
if (mimeType.startsWith('image/')) return <Image className="w-5 h-5 text-white" />;
|
||||
if (mimeType.startsWith('video/')) return <Video className="w-5 h-5 text-white" />;
|
||||
if (mimeType.startsWith('audio/')) return <Music className="w-5 h-5 text-white" />;
|
||||
if (mimeType.includes('zip') || mimeType.includes('rar')) return <Archive className="w-5 h-5 text-white" />;
|
||||
return <FileText className="w-5 h-5 text-white" />;
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
interface WebRTCFileUploadProps {
|
||||
selectedFiles: File[];
|
||||
onFilesChange: (files: File[]) => void;
|
||||
onGenerateCode: () => void;
|
||||
pickupCode?: string;
|
||||
pickupLink?: string;
|
||||
onCopyCode?: () => void;
|
||||
onCopyLink?: () => void;
|
||||
onAddMoreFiles?: () => void;
|
||||
onRemoveFile?: (updatedFiles: File[]) => void;
|
||||
onClearFiles?: () => void;
|
||||
onReset?: () => void;
|
||||
disabled?: boolean;
|
||||
isConnected?: boolean;
|
||||
isWebSocketConnected?: boolean;
|
||||
}
|
||||
|
||||
export function WebRTCFileUpload({
|
||||
selectedFiles,
|
||||
onFilesChange,
|
||||
onGenerateCode,
|
||||
pickupCode,
|
||||
pickupLink,
|
||||
onCopyCode,
|
||||
onCopyLink,
|
||||
onAddMoreFiles,
|
||||
onRemoveFile,
|
||||
onClearFiles,
|
||||
onReset,
|
||||
disabled = false,
|
||||
isConnected = false,
|
||||
isWebSocketConnected = false
|
||||
}: WebRTCFileUploadProps) {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||
setIsDragOver(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
onFilesChange([...selectedFiles, ...files]);
|
||||
}
|
||||
}, [selectedFiles, onFilesChange]);
|
||||
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length > 0) {
|
||||
onFilesChange([...selectedFiles, ...files]);
|
||||
}
|
||||
}, [selectedFiles, onFilesChange]);
|
||||
|
||||
const removeFile = useCallback((index: number) => {
|
||||
const updatedFiles = selectedFiles.filter((_, i) => i !== index);
|
||||
onFilesChange(updatedFiles);
|
||||
if (onRemoveFile) {
|
||||
onRemoveFile(updatedFiles);
|
||||
}
|
||||
}, [selectedFiles, onFilesChange, onRemoveFile]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 如果没有选择文件,显示上传区域
|
||||
if (selectedFiles.length === 0 && !pickupCode) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 功能标题和状态 */}
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-xl flex items-center justify-center">
|
||||
<Upload className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800">选择文件</h2>
|
||||
<p className="text-sm text-slate-600">拖拽文件到下方区域或点击选择文件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 竖线分割 */}
|
||||
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
||||
|
||||
{/* 状态显示 */}
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
||||
<div className="flex items-center justify-end space-x-3 text-sm">
|
||||
{/* WebSocket状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
||||
<span className="text-slate-600">WS</span>
|
||||
</div>
|
||||
|
||||
{/* 分隔符 */}
|
||||
<div className="text-slate-300">|</div>
|
||||
|
||||
{/* WebRTC状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
||||
<span className="text-slate-600">RTC</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`upload-area rounded-xl p-6 sm:p-8 md:p-12 text-center cursor-pointer ${
|
||||
isDragOver ? 'drag-active' : ''
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={`transition-all duration-300 ${isDragOver ? 'scale-110' : ''}`}>
|
||||
<div className="w-16 h-16 sm:w-20 sm:h-20 mx-auto mb-4 sm:mb-6 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-full flex items-center justify-center">
|
||||
<Upload className={`w-8 h-8 sm:w-10 sm:h-10 transition-colors duration-300 ${
|
||||
isDragOver ? 'text-blue-600' : 'text-slate-400'
|
||||
}`} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-lg sm:text-xl font-medium text-slate-700">
|
||||
{isDragOver ? '释放文件' : '拖拽文件到这里'}
|
||||
</p>
|
||||
<p className="text-sm sm:text-base text-slate-500">
|
||||
或者 <span className="text-blue-600 font-medium underline">点击选择文件</span>
|
||||
</p>
|
||||
<p className="text-xs sm:text-sm text-slate-400 mt-4">
|
||||
支持多个文件同时上传,WebRTC点对点传输
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* 文件列表 */}
|
||||
<div>
|
||||
{/* 功能标题和状态 */}
|
||||
<div className="flex items-center mb-4 sm:mb-6">
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800">已选择文件</h3>
|
||||
<p className="text-sm text-slate-500">{selectedFiles.length} 个文件准备传输</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 竖线分割 */}
|
||||
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
||||
|
||||
{/* 状态显示 */}
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
||||
<div className="flex items-center justify-end space-x-3 text-sm">
|
||||
{/* WebSocket状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isWebSocketConnected ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-emerald-600">WS</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
||||
<span className="text-slate-600">WS</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分隔符 */}
|
||||
<div className="text-slate-300">|</div>
|
||||
|
||||
{/* WebRTC状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isConnected ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-emerald-600">RTC</span>
|
||||
</>
|
||||
) : pickupCode ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
|
||||
<span className="text-orange-600">RTC</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
||||
<span className="text-slate-600">RTC</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4 sm:mb-6">
|
||||
{selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={`${file.name}-${file.size}-${index}`}
|
||||
className="group flex items-center justify-between p-3 sm:p-4 bg-gradient-to-r from-slate-50 to-blue-50 border border-slate-200 rounded-xl hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center space-x-3 sm:space-x-4 min-w-0 flex-1">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
{getFileIcon(file.type)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-slate-800 truncate text-sm sm:text-base">{file.name}</p>
|
||||
<p className="text-xs sm:text-sm text-slate-500">{formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={disabled}
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all duration-200 flex-shrink-0 ml-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
{!pickupCode && (
|
||||
<>
|
||||
<Button
|
||||
onClick={onGenerateCode}
|
||||
disabled={disabled || selectedFiles.length === 0}
|
||||
className="button-primary text-white px-6 sm:px-8 py-3 rounded-xl font-medium flex-1 min-w-0 shadow-lg"
|
||||
>
|
||||
<Upload className="w-5 h-5 mr-2" />
|
||||
生成取件码
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onAddMoreFiles}
|
||||
variant="outline"
|
||||
disabled={disabled}
|
||||
className="px-6 sm:px-8 py-3 rounded-xl font-medium"
|
||||
>
|
||||
添加文件
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onReset}
|
||||
variant="outline"
|
||||
disabled={disabled}
|
||||
className="text-red-600 hover:bg-red-50 px-6 sm:px-8 py-3 rounded-xl font-medium"
|
||||
>
|
||||
重新选择
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{pickupCode && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onAddMoreFiles}
|
||||
disabled={disabled}
|
||||
className="px-6 py-3 rounded-xl border-slate-300 text-slate-600 hover:bg-slate-50 flex-1"
|
||||
>
|
||||
添加更多文件
|
||||
</Button>
|
||||
|
||||
{selectedFiles.length > 0 && onClearFiles && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClearFiles}
|
||||
disabled={disabled}
|
||||
className="px-6 py-3 rounded-xl border-orange-300 text-orange-600 hover:bg-orange-50"
|
||||
>
|
||||
清空文件
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onReset}
|
||||
disabled={disabled}
|
||||
className="px-6 py-3 rounded-xl border-red-300 text-red-600 hover:bg-red-50"
|
||||
>
|
||||
关闭房间
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 取件码展示 */}
|
||||
{pickupCode && (
|
||||
<div className="border-t border-slate-200 pt-6">
|
||||
<div className="text-center mb-4 sm:mb-6">
|
||||
<div className="w-12 h-12 sm:w-16 sm:h-16 mx-auto mb-4 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-2xl flex items-center justify-center animate-float">
|
||||
<FileText className="w-6 h-6 sm:w-8 sm:h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl sm:text-2xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent mb-2">
|
||||
取件码生成成功!
|
||||
</h3>
|
||||
<p className="text-sm sm:text-base text-slate-600">分享以下信息给接收方</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* 取件码 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">取件码</label>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="flex-1 code-display rounded-xl p-4 sm:p-6 text-center">
|
||||
<div className="text-2xl sm:text-3xl font-bold font-mono bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent tracking-wider">
|
||||
{pickupCode}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onCopyCode}
|
||||
className="px-4 sm:px-6 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-medium shadow-lg transition-all duration-200 hover:shadow-xl w-full sm:w-auto"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 取件链接 */}
|
||||
{pickupLink && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">取件链接</label>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="flex-1 code-display rounded-xl p-3 sm:p-4">
|
||||
<div className="text-xs sm:text-sm text-slate-700 break-all font-mono">
|
||||
{pickupLink}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onCopyLink}
|
||||
className="px-4 sm:px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-xl font-medium shadow-lg transition-all duration-200 hover:shadow-xl w-full sm:w-auto"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 使用提示 */}
|
||||
<div className="mt-4 sm:mt-6 p-3 sm:p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200">
|
||||
<p className="text-xs sm:text-sm text-slate-600 text-center">
|
||||
💡 <span className="font-medium">使用提示:</span>接收方输入取件码或访问取件链接即可下载文件
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user