From 69dc78300e5edde40cbcc026d6b19ce2bb15f361 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 15:00:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=B2=E7=9B=92=E6=88=BF=E9=97=B4?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=A0=E9=99=A4=EF=BC=88=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E8=80=85=EF=BC=89=E5=92=8C=E9=80=80=E5=87=BA=EF=BC=88=E6=88=90?= =?UTF-8?q?=E5=91=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DELETE /api/blindbox/room/[code] 根据身份区分行为 - 房间页底部两步确认按钮,防止误操作 - 更新 ROADMAP:该功能从 P3 提升至 P1,移除低价值项 --- ROADMAP.md | 9 ++-- src/app/api/blindbox/room/[code]/route.ts | 33 ++++++++++++ src/app/blindbox/[code]/page.tsx | 65 +++++++++++++++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 3b2197d..498edc5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,6 +38,10 @@ - Service Worker 离线缓存基础页面 - `viewport-fit=cover` 适配刘海屏 +### 盲盒房间删除 / 退出 +- 房间创建者可删除房间(级联清理成员 & 想法) +- 非创建者可退出房间(从成员列表移除自己) + ### 首次体验引导优化 - ~~极速救场完成一轮后引导注册("注册保存记录")~~(已完成,匹配成功页展示注册卡片,注册后自动保存记录) - 盲盒模式先展示 demo / 动画,让用户看到价值再引导注册 @@ -78,11 +82,6 @@ - 盲盒投放数量成就 - 在个人中心展示,增加用户粘性 -### 盲盒房间生命周期 -- 闲置 30 天自动归档 -- 支持删除 / 退出房间 -- 房间设置页(修改名称、管理成员、清空想法池) - ### 浅色模式 - 当前暗色主题是唯一选项 - 白天户外使用体验差 diff --git a/src/app/api/blindbox/room/[code]/route.ts b/src/app/api/blindbox/room/[code]/route.ts index 8baa90b..498b199 100644 --- a/src/app/api/blindbox/room/[code]/route.ts +++ b/src/app/api/blindbox/room/[code]/route.ts @@ -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); + } +} diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index f0938aa..fda5a94 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -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(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 (
@@ -620,6 +654,37 @@ export default function BlindboxRoomPage() { /> )} + {/* Leave / Delete */} + {isMember && room && ( + + + + )} + {/* Toast */} {toast && (