diff --git a/write-server/.gitignore b/write-server/.gitignore index 685cf002..eb93eabe 100644 --- a/write-server/.gitignore +++ b/write-server/.gitignore @@ -38,7 +38,7 @@ Thumbs.db # Project specific logs/ backups/ -recycle/ +/recycle/ # Docker docker-compose.override.yml diff --git a/write-server/src/app/api/recycle/route.ts b/write-server/src/app/api/recycle/route.ts new file mode 100644 index 00000000..159c084b --- /dev/null +++ b/write-server/src/app/api/recycle/route.ts @@ -0,0 +1,66 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + listRecycle, + restoreFromRecycle, + permanentDelete, +} from "@/lib/recycle"; + +export async function GET() { + try { + const items = listRecycle(); + return NextResponse.json({ items }); + } catch (error: unknown) { + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { slug } = body; + if (!slug) { + return NextResponse.json({ error: "Slug is required" }, { status: 400 }); + } + + const success = restoreFromRecycle(slug); + if (!success) { + return NextResponse.json( + { error: "Item not found in recycle bin" }, + { status: 404 } + ); + } + return NextResponse.json({ success: true }); + } catch (error: unknown) { + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} + +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = request.nextUrl; + const slug = searchParams.get("slug"); + if (!slug) { + return NextResponse.json({ error: "Slug is required" }, { status: 400 }); + } + + const success = permanentDelete(slug); + if (!success) { + return NextResponse.json( + { error: "Item not found in recycle bin" }, + { status: 404 } + ); + } + return NextResponse.json({ success: true }); + } catch (error: unknown) { + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/write-server/src/app/recycle/page.tsx b/write-server/src/app/recycle/page.tsx new file mode 100644 index 00000000..e9c6944b --- /dev/null +++ b/write-server/src/app/recycle/page.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Trash2, RotateCcw, AlertTriangle } from "lucide-react"; + +interface RecycleItem { + slug: string; title: string; deletedAt: string; daysRemaining: number; +} + +export default function RecyclePage() { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); + + const fetchItems = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/recycle"); + setItems((await res.json()).items || []); + } catch {} finally { setLoading(false); } + }, []); + + useEffect(() => { fetchItems(); }, [fetchItems]); + + const handleRestore = async (slug: string, title: string) => { + if (!confirm(`确定恢复「${title}」?`)) return; + setMessage(null); + try { + const res = await fetch("/api/recycle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ slug }) }); + if (!res.ok) throw new Error("恢复失败"); + setMessage({ type: "success", text: `已恢复「${title}」` }); + fetchItems(); + } catch (err: unknown) { + setMessage({ type: "error", text: err instanceof Error ? err.message : "恢复失败" }); + } + }; + + const handlePermanentDelete = async (slug: string, title: string) => { + if (!confirm(`永久删除「${title}」?此操作不可撤销!`)) return; + setMessage(null); + try { + const res = await fetch(`/api/recycle?slug=${encodeURIComponent(slug)}`, { method: "DELETE" }); + if (!res.ok) throw new Error("删除失败"); + setMessage({ type: "success", text: `已永久删除「${title}」` }); + fetchItems(); + } catch (err: unknown) { + setMessage({ type: "error", text: err instanceof Error ? err.message : "删除失败" }); + } + }; + + return ( +
+
+

回收站

+

删除的文章保留 30 天,到期自动清理

+
+ + {message && ( +
+ {message.text} + +
+ )} + + {loading ? ( +
加载中...
+ ) : items.length === 0 ? ( +
+ +

回收站为空

+
+ ) : ( +
+ {items.map((item) => ( +
+
+ {item.title} +
+ 删除于 {new Date(item.deletedAt).toLocaleString("zh-CN")} + {item.daysRemaining <= 3 ? ( + + 剩余 {item.daysRemaining} 天 + + ) : ( + 剩余 {item.daysRemaining} 天 + )} +
+
+
+ + +
+
+ ))} +
+ )} +
+ ); +}