fenghuo/apps/web/components/FileUpload.tsx

219 lines
6.5 KiB
TypeScript
Raw Normal View History

2025-05-28 20:00:36 +08:00
'use client';
import React, { useCallback, useState } from 'react';
import { useTusUpload } from '../hooks/useTusUpload';
interface UploadedFile {
fileId: string;
fileName: string;
url: string;
}
export function FileUpload() {
const { uploadProgress, isUploading, uploadError, handleFileUpload, getFileUrlByFileId, serverUrl } = useTusUpload();
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
const [dragOver, setDragOver] = useState(false);
// 处理文件选择
const handleFileSelect = useCallback(
async (files: FileList | null) => {
if (!files || files.length === 0) return;
for (let i = 0; i < files.length; i++) {
const file = files[i];
try {
const result = await handleFileUpload(
file,
(result) => {
console.log('Upload success:', result);
setUploadedFiles((prev) => [
...prev,
{
fileId: result.fileId,
fileName: result.fileName,
url: result.url,
},
]);
},
(error) => {
console.error('Upload error:', error);
},
);
} catch (error) {
console.error('Upload failed:', error);
}
}
},
[handleFileUpload],
);
// 处理拖拽上传
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
handleFileSelect(e.dataTransfer.files);
},
[handleFileSelect],
);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
}, []);
// 处理文件输入
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
handleFileSelect(e.target.files);
},
[handleFileSelect],
);
// 复制链接到剪贴板
const copyToClipboard = useCallback(async (url: string) => {
try {
await navigator.clipboard.writeText(url);
alert('链接已复制到剪贴板!');
} catch (error) {
console.error('Failed to copy:', error);
}
}, []);
return (
<div className="max-w-2xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-6"></h2>
{/* 服务器信息 */}
<div className="mb-4 p-3 bg-gray-100 rounded-lg">
<p className="text-sm text-gray-600">
: <span className="font-mono">{serverUrl}</span>
</p>
</div>
{/* 拖拽上传区域 */}
<div
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
dragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
<div className="space-y-4">
<div className="text-gray-500">
<svg className="mx-auto h-12 w-12" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div>
<p className="text-lg font-medium text-gray-900"></p>
<label className="cursor-pointer">
<span className="mt-2 block text-sm font-medium text-blue-600 hover:text-blue-500"></span>
<input type="file" multiple className="hidden" onChange={handleInputChange} disabled={isUploading} />
</label>
</div>
<p className="text-xs text-gray-500">TUS </p>
</div>
</div>
{/* 上传进度 */}
{isUploading && (
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-blue-900">...</span>
<span className="text-sm text-blue-600">{uploadProgress}%</span>
</div>
<div className="w-full bg-blue-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
</div>
</div>
)}
{/* 错误信息 */}
{uploadError && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">
<span className="font-medium"></span>
{uploadError}
</p>
</div>
)}
{/* 已上传文件列表 */}
{uploadedFiles.length > 0 && (
<div className="mt-6">
<h3 className="text-lg font-medium mb-4"></h3>
<div className="space-y-3">
{uploadedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-4 bg-green-50 border border-green-200 rounded-lg"
>
<div className="flex items-center space-x-3">
<div className="flex-shrink-0">
<svg className="h-8 w-8 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div>
<p className="text-sm font-medium text-gray-900">{file.fileName}</p>
<p className="text-xs text-gray-500">ID: {file.fileId}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
>
</a>
<button
onClick={() => copyToClipboard(file.url)}
className="text-gray-600 hover:text-gray-800 text-sm font-medium"
>
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* 使用说明 */}
<div className="mt-8 p-4 bg-gray-50 rounded-lg">
<h4 className="text-sm font-medium text-gray-900 mb-2">使</h4>
<ul className="text-xs text-gray-600 space-y-1">
<li> </li>
<li> 使 TUS </li>
<li> 访</li>
<li> PDF </li>
<li> </li>
</ul>
</div>
</div>
);
}