feat: 盲盒房间支持删除(创建者)和退出(成员)

- DELETE /api/blindbox/room/[code] 根据身份区分行为
- 房间页底部两步确认按钮,防止误操作
- 更新 ROADMAP:该功能从 P3 提升至 P1,移除低价值项
This commit is contained in:
2026-02-26 15:00:30 +08:00
parent 05e42ffe22
commit 69dc78300e
3 changed files with 102 additions and 5 deletions
+33
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { errorResponse, getRoomByCode } from "@/lib/blindbox";
export async function GET(
@@ -28,3 +29,35 @@ export async function GET(
return errorResponse("获取房间信息失败", 500);
}
}
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ code: string }> },
) {
try {
const { code } = await params;
const { userId } = await req.json();
if (!userId) return errorResponse("缺少用户 ID", 400);
const room = await prisma.blindBoxRoom.findUnique({
where: { code: code.toUpperCase() },
});
if (!room) return errorResponse("房间不存在", 404);
if (room.creatorId === userId) {
await prisma.blindBoxRoom.delete({ where: { id: room.id } });
return NextResponse.json({ action: "deleted" });
}
const membership = await prisma.blindBoxMember.findUnique({
where: { roomId_userId: { roomId: room.id, userId } },
});
if (!membership) return errorResponse("你不是该房间成员", 403);
await prisma.blindBoxMember.delete({ where: { id: membership.id } });
return NextResponse.json({ action: "left" });
} catch {
return errorResponse("操作失败", 500);
}
}
+65
View File
@@ -14,6 +14,8 @@ import {
Share2,
LogIn,
Copy,
Trash2,
LogOut,
} from "lucide-react";
import confetti from "canvas-confetti";
import { getCachedProfile, isRegistered } from "@/lib/userId";
@@ -60,6 +62,8 @@ export default function BlindboxRoomPage() {
const [showInvite, setShowInvite] = useState(false);
const [showShareCard, setShowShareCard] = useState(false);
const [toast, setToast] = useState("");
const [confirmLeave, setConfirmLeave] = useState(false);
const [leaving, setLeaving] = useState(false);
const boxControls = useAnimation();
const inputRef = useRef<HTMLInputElement>(null);
@@ -250,6 +254,36 @@ export default function BlindboxRoomPage() {
handleCopyCode();
};
const isCreator = profile?.id === room?.creatorId;
const handleLeaveOrDelete = async () => {
if (!confirmLeave) {
setConfirmLeave(true);
setTimeout(() => setConfirmLeave(false), 3000);
return;
}
if (leaving || !profile || !room) return;
setLeaving(true);
try {
const res = await fetch(`/api/blindbox/room/${room.code}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId: profile.id }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "操作失败");
}
router.replace("/blindbox");
} catch (e) {
setToast(e instanceof Error ? e.message : "操作失败");
setTimeout(() => setToast(""), 2200);
setConfirmLeave(false);
} finally {
setLeaving(false);
}
};
if (pageLoading) {
return (
<div className="flex min-h-dvh items-center justify-center bg-background">
@@ -620,6 +654,37 @@ export default function BlindboxRoomPage() {
/>
)}
{/* Leave / Delete */}
{isMember && room && (
<motion.div
className="mt-12 w-full max-w-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<button
onClick={handleLeaveOrDelete}
disabled={leaving}
className={`flex w-full items-center justify-center gap-2 rounded-xl py-2.5 text-xs font-medium transition-colors ${
confirmLeave
? "bg-rose-600/15 text-rose-400 ring-1 ring-rose-500/30"
: "text-muted hover:text-rose-400/80"
}`}
>
{leaving ? (
<Loader2 size={13} className="animate-spin" />
) : isCreator ? (
<Trash2 size={13} />
) : (
<LogOut size={13} />
)}
{confirmLeave
? isCreator ? "确认删除房间?所有想法将被清除" : "确认退出房间?"
: isCreator ? "删除房间" : "退出房间"}
</button>
</motion.div>
)}
{/* Toast */}
<AnimatePresence>
{toast && (