Files
my_site/front/src/pages/RecycleBin.tsx

166 lines
6.8 KiB
TypeScript
Raw 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.
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Clock3, Folder, RefreshCw, RotateCcw, Trash2 } from 'lucide-react';
import { Button } from '@/src/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/src/components/ui/card';
import { apiRequest } from '@/src/lib/api';
import type { PageResponse, RecycleBinItem } from '@/src/lib/types';
import { formatRecycleBinExpiresLabel, RECYCLE_BIN_RETENTION_DAYS } from './recycle-bin-state';
function formatFileSize(size: number) {
if (size <= 0) {
return '—';
}
const units = ['B', 'KB', 'MB', 'GB'];
const index = Math.min(Math.floor(Math.log(size) / Math.log(1024)), units.length - 1);
const value = size / 1024 ** index;
return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
}
function formatDateTime(value: string) {
return new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(value));
}
export default function RecycleBin() {
const [items, setItems] = useState<RecycleBinItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [restoringId, setRestoringId] = useState<number | null>(null);
const loadRecycleBin = async () => {
setLoading(true);
setError('');
try {
const response = await apiRequest<PageResponse<RecycleBinItem>>('/files/recycle-bin?page=0&size=100');
setItems(response.items);
} catch (requestError) {
setError(requestError instanceof Error ? requestError.message : '回收站加载失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
void loadRecycleBin();
}, []);
const handleRestore = async (itemId: number) => {
setRestoringId(itemId);
setError('');
try {
await apiRequest(`/files/recycle-bin/${itemId}/restore`, {
method: 'POST',
});
setItems((previous) => previous.filter((item) => item.id !== itemId));
} catch (requestError) {
setError(requestError instanceof Error ? requestError.message : '恢复失败');
} finally {
setRestoringId(null);
}
};
return (
<div className="mx-auto flex h-full w-full max-w-6xl flex-col gap-6">
<Card className="overflow-hidden">
<CardHeader className="flex flex-col gap-4 border-b border-white/10 sm:flex-row sm:items-end sm:justify-between">
<div className="space-y-2">
<div className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs text-slate-300">
<Trash2 className="h-3.5 w-3.5" />
{RECYCLE_BIN_RETENTION_DAYS}
</div>
<CardTitle className="text-2xl text-white"></CardTitle>
<p className="text-sm text-slate-400">
{RECYCLE_BIN_RETENTION_DAYS}
</p>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
className="border-white/10 bg-white/5 text-slate-200 hover:bg-white/10"
onClick={() => void loadRecycleBin()}
disabled={loading}
>
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
<Link
to="/files"
className="inline-flex h-10 items-center justify-center rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-slate-200 transition-colors hover:bg-white/10"
>
</Link>
</div>
</CardHeader>
<CardContent className="p-6">
{error ? (
<div className="mb-4 rounded-2xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-200">
{error}
</div>
) : null}
{loading ? (
<div className="flex min-h-64 items-center justify-center text-sm text-slate-400">
...
</div>
) : items.length === 0 ? (
<div className="flex min-h-64 flex-col items-center justify-center gap-4 rounded-3xl border border-dashed border-white/10 bg-black/10 text-center">
<div className="rounded-3xl border border-white/10 bg-white/5 p-4">
<Trash2 className="h-8 w-8 text-slate-400" />
</div>
<div className="space-y-1">
<p className="text-lg font-medium text-white"></p>
<p className="text-sm text-slate-400"> 10 </p>
</div>
</div>
) : (
<div className="space-y-4">
{items.map((item) => (
<div
key={item.id}
className="flex flex-col gap-4 rounded-3xl border border-white/10 bg-black/10 p-5 lg:flex-row lg:items-center lg:justify-between"
>
<div className="min-w-0 space-y-3">
<div className="flex items-center gap-3">
<div className="rounded-2xl border border-white/10 bg-white/5 p-3 text-slate-200">
<Folder className="h-5 w-5" />
</div>
<div className="min-w-0">
<p className="truncate text-base font-semibold text-white">{item.filename}</p>
<p className="truncate text-sm text-slate-400">{item.path}</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-400">
<span>{item.directory ? '文件夹' : formatFileSize(item.size)}</span>
<span> {formatDateTime(item.deletedAt)}</span>
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/20 bg-amber-500/10 px-2.5 py-1 text-amber-200">
<Clock3 className="h-3.5 w-3.5" />
{formatRecycleBinExpiresLabel(item.expiresAt)}
</span>
</div>
</div>
<Button
className="min-w-28 self-start bg-[#336EFF] text-white hover:bg-[#2958cc] lg:self-center"
onClick={() => void handleRestore(item.id)}
disabled={restoringId === item.id}
>
<RotateCcw className="mr-2 h-4 w-4" />
{restoringId === item.id ? '恢复中' : '恢复'}
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}