Files
XCDesktop/src/modules/recycle-bin/RecycleBinPage.tsx

284 lines
8.8 KiB
TypeScript

import React, { useEffect, useState, useRef } from 'react'
import { Trash2, FileText, Folder, Loader2 } from 'lucide-react'
import { useWallpaper } from '@/stores'
import { fetchRecycleBin, restoreItem, permanentlyDeleteItem, emptyRecycleBin } from './api'
import type { RecycleBinGroupDTO, RecycleBinItemDTO } from '@shared/types'
import { ContextMenu } from '@/components/common/ContextMenu'
import { DeleteConfirmDialog } from '@/components/dialogs/DeleteConfirmDialog'
type NotebookEvent = {
event: string
path?: string
}
const formatDate = (dateStr: string): string => {
if (dateStr.length !== 8) return dateStr
const year = dateStr.substring(0, 4)
const month = dateStr.substring(4, 6)
const day = dateStr.substring(6, 8)
return `${year}${month}${day}`
}
export const RecycleBinPage: React.FC = () => {
const { opacity } = useWallpaper()
const [groups, setGroups] = useState<RecycleBinGroupDTO[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [contextMenu, setContextMenu] = useState<{
isOpen: boolean
position: { x: number; y: number }
item: RecycleBinItemDTO | null
}>({
isOpen: false,
position: { x: 0, y: 0 },
item: null
})
const [deleteDialog, setDeleteDialog] = useState<{
isOpen: boolean
item: RecycleBinItemDTO | null
}>({
isOpen: false,
item: null
})
const [emptyDialog, setEmptyDialog] = useState(false)
const loadRecycleBinRef = useRef<(showLoading?: boolean) => Promise<void>>()
const loadRecycleBin = async (showLoading = true) => {
if (showLoading) {
setLoading(true)
}
setError(null)
try {
const data = await fetchRecycleBin()
setGroups(data.groups)
} catch (err) {
console.error('Failed to load recycle bin:', err)
setError('加载回收站失败')
} finally {
if (showLoading) {
setLoading(false)
}
}
}
loadRecycleBinRef.current = loadRecycleBin
useEffect(() => {
loadRecycleBin()
}, [])
useEffect(() => {
const eventSource = new EventSource('/api/events')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data) as NotebookEvent
if (
data.event === 'change' ||
data.event === 'add' ||
data.event === 'unlink' ||
data.event === 'addDir' ||
data.event === 'unlinkDir'
) {
if (data.path && data.path.startsWith('RB/')) {
loadRecycleBinRef.current?.(false)
}
}
}
eventSource.onerror = () => {
eventSource.close()
}
return () => {
eventSource.close()
}
}, [])
const handleContextMenu = (e: React.MouseEvent, item: RecycleBinItemDTO) => {
e.preventDefault()
e.stopPropagation()
setContextMenu({
isOpen: true,
position: { x: e.clientX, y: e.clientY },
item
})
}
const handleCloseContextMenu = () => {
setContextMenu(prev => ({ ...prev, isOpen: false }))
}
const handleRestore = async () => {
if (!contextMenu.item) return
try {
await restoreItem(contextMenu.item.path, contextMenu.item.type)
handleCloseContextMenu()
loadRecycleBin(false)
} catch (err) {
console.error('Failed to restore:', err)
alert('恢复失败')
}
}
const handlePermanentDeleteClick = () => {
if (!contextMenu.item) return
handleCloseContextMenu()
setDeleteDialog({
isOpen: true,
item: contextMenu.item
})
}
const handlePermanentDeleteConfirm = async () => {
if (!deleteDialog.item) return
try {
await permanentlyDeleteItem(deleteDialog.item.path, deleteDialog.item.type)
setDeleteDialog({ isOpen: false, item: null })
loadRecycleBin(false)
} catch (err) {
console.error('Failed to delete:', err)
alert('删除失败')
}
}
const handleDeleteDialogCancel = () => {
setDeleteDialog({ isOpen: false, item: null })
}
const handleEmptyClick = () => {
setEmptyDialog(true)
}
const handleEmptyConfirm = async () => {
try {
await emptyRecycleBin()
setEmptyDialog(false)
loadRecycleBin(false)
} catch (err) {
console.error('Failed to empty recycle bin:', err)
alert('清空回收站失败')
}
}
const handleEmptyDialogCancel = () => {
setEmptyDialog(false)
}
const contextMenuItems = [
{ label: '恢复', onClick: handleRestore },
{ label: '彻底删除', onClick: handlePermanentDeleteClick }
]
const deleteItem = deleteDialog.item
const deleteExpectedText = deleteItem?.type === 'dir' ? 'DESTROY FOLDER' : 'DESTROY FILE'
const deleteTitle = deleteItem?.type === 'dir' ? '彻底删除文件夹' : '彻底删除文件'
const deleteMessage = deleteItem?.type === 'dir'
? `确定要彻底删除文件夹 "${deleteItem?.originalName}" 吗?此操作不可撤销,文件夹中的所有内容都将被永久删除。`
: `确定要彻底删除文件 "${deleteItem?.originalName}" 吗?此操作不可撤销,文件将被永久删除。`
return (
<div className="max-w-4xl mx-auto w-full px-14 py-12 pb-40">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<Trash2 size={28} className="text-gray-600 dark:text-gray-300" />
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200"></h1>
</div>
{groups.length > 0 && (
<button
onClick={handleEmptyClick}
className="px-4 py-2 text-sm bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 rounded-lg transition-colors"
>
</button>
)}
</div>
{loading && (
<div className="flex items-center justify-center py-12">
<Loader2 size={32} className="animate-spin text-gray-400" />
</div>
)}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
{error}
</div>
)}
{!loading && !error && groups.length === 0 && (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
<Trash2 size={48} className="mx-auto mb-4 opacity-30" />
<p></p>
</div>
)}
{!loading && !error && groups.length > 0 && (
<div className="space-y-6">
{groups.map((group) => (
<div
key={group.date}
className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-xl border border-gray-200/50 dark:border-gray-700/60 shadow-sm overflow-hidden"
>
<div
className="px-4 py-3 border-b border-gray-200/50 dark:border-gray-700/60 font-medium text-gray-700 dark:text-gray-200"
style={{
backgroundColor: `rgba(var(--app-content-bg-rgb), ${opacity * 0.8})`
}}
>
{formatDate(group.date)}
</div>
<div className="divide-y divide-gray-200/50 dark:divide-gray-700/60">
{group.items.map((item) => (
<div
key={item.path}
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-100/50 dark:hover:bg-gray-800/50 transition-colors cursor-pointer"
onContextMenu={(e) => handleContextMenu(e, item)}
>
{item.type === 'dir' ? (
<Folder size={20} className="text-gray-500 dark:text-gray-400 shrink-0" />
) : (
<FileText size={20} className="text-gray-500 dark:text-gray-400 shrink-0" />
)}
<span className="text-gray-700 dark:text-gray-200 truncate">
{item.originalName}
</span>
</div>
))}
</div>
</div>
))}
</div>
)}
<ContextMenu
isOpen={contextMenu.isOpen}
position={contextMenu.position}
items={contextMenuItems}
onClose={handleCloseContextMenu}
/>
<DeleteConfirmDialog
isOpen={deleteDialog.isOpen}
title={deleteTitle}
message={deleteMessage}
expectedText={deleteExpectedText}
confirmText="彻底删除"
onConfirm={handlePermanentDeleteConfirm}
onCancel={handleDeleteDialogCancel}
/>
<DeleteConfirmDialog
isOpen={emptyDialog}
title="清空回收站"
message="确定要清空回收站吗?此操作将永久删除回收站中的所有文件和文件夹,且不可撤销。"
expectedText="DESTROY EVERYTHING"
confirmText="清空"
buttonVariant="primary"
onConfirm={handleEmptyConfirm}
onCancel={handleEmptyDialogCancel}
/>
</div>
)
}