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];
|
2025-06-05 15:44:06 +08:00
|
|
|
|
if (!file) continue;
|
2025-05-28 20:00:36 +08:00
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|