gongxiangp2/components/ResumeUploadModal.tsx

207 lines
7.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useEffect } from 'react'
import { getAllPendingUploads, deleteUploadState } from '@/lib/chunk-upload'
// 格式化字节数
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}
interface ChunkUploadState {
fileId: string
fileName: string
fileSize: number
chunkSize: number
totalChunks: number
uploadedChunks: Set<number>
uploadId?: string
metadata?: any
}
interface ResumeUploadModalProps {
onClose: () => void
onResume: (fileId: string, fileName: string, fileSize: number, metadata?: any) => void
onPendingChange?: (count: number) => void
}
export default function ResumeUploadModal({ onClose, onResume, onPendingChange }: ResumeUploadModalProps) {
const [pendingUploads, setPendingUploads] = useState<ChunkUploadState[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
loadPendingUploads()
}, [])
const loadPendingUploads = async () => {
try {
const uploads = await getAllPendingUploads()
// 将Set转换为数组以便序列化
const serializedUploads = uploads.map(upload => ({
...upload,
uploadedChunks: Array.from(upload.uploadedChunks),
}))
setPendingUploads(serializedUploads as any)
onPendingChange?.(serializedUploads.length)
if (serializedUploads.length === 0) {
onClose()
}
} catch (error) {
console.error('[ResumeUploadModal] Failed to load pending uploads:', error)
} finally {
setLoading(false)
}
}
const handleResume = (fileId: string, fileName: string, fileSize: number, metadata?: any) => {
onResume(fileId, fileName, fileSize, metadata)
onClose()
}
const handleCancel = async (fileId: string) => {
try {
await deleteUploadState(fileId)
await loadPendingUploads()
} catch (error) {
console.error('[ResumeUploadModal] Failed to cancel upload:', error)
}
}
const handleCancelAll = async () => {
try {
for (const upload of pendingUploads) {
await deleteUploadState(upload.fileId)
}
await loadPendingUploads()
if (pendingUploads.length === 0) {
onClose()
}
} catch (error) {
console.error('[ResumeUploadModal] Failed to cancel all uploads:', error)
}
}
const getProgress = (upload: ChunkUploadState) => {
const uploadedCount = Array.isArray(upload.uploadedChunks)
? upload.uploadedChunks.length
: upload.uploadedChunks.size || 0
return Math.round((uploadedCount / upload.totalChunks) * 100)
}
if (loading) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">检查未完成的上传...</p>
</div>
</div>
</div>
)
}
if (pendingUploads.length === 0) {
return null
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
发现未完成的上传
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p className="text-gray-600 dark:text-gray-400 mb-4">
检测到 {pendingUploads.length} 个未完成的上传任务,可随时恢复继续。
<br />
<span className="text-sm text-gray-500 dark:text-gray-400">提示:您仍可在其他页面继续新增上传。</span>
</p>
<div className="space-y-3 mb-4">
{pendingUploads.map((upload) => {
const progress = getProgress(upload)
const uploadedCount = Array.isArray(upload.uploadedChunks)
? upload.uploadedChunks.length
: upload.uploadedChunks.size || 0
return (
<div
key={upload.fileId}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4"
>
<div className="flex justify-between items-start mb-2">
<div className="flex-1">
<p className="font-medium text-gray-900 dark:text-white truncate">
{upload.fileName}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{formatBytes(upload.fileSize)} • {uploadedCount}/{upload.totalChunks} 块已上传
</p>
</div>
<div className="ml-4 text-right">
<p className="text-sm font-medium text-gray-900 dark:text-white">
{progress}%
</p>
</div>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-3">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
></div>
</div>
<div className="flex gap-2">
<button
onClick={() => handleResume(upload.fileId, upload.fileName, upload.fileSize, (upload as any).metadata)}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
继续上传
</button>
<button
onClick={() => handleCancel(upload.fileId)}
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
取消
</button>
</div>
</div>
)
})}
</div>
<div className="flex gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
onClick={onClose}
className="flex-1 px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
稍后处理
</button>
<button
onClick={handleCancelAll}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
全部取消
</button>
</div>
</div>
</div>
)
}