feat: 盲盒房间支持删除(创建者)和退出(成员)
- DELETE /api/blindbox/room/[code] 根据身份区分行为 - 房间页底部两步确认按钮,防止误操作 - 更新 ROADMAP:该功能从 P3 提升至 P1,移除低价值项
This commit is contained in:
+4
-5
@@ -38,6 +38,10 @@
|
|||||||
- Service Worker 离线缓存基础页面
|
- Service Worker 离线缓存基础页面
|
||||||
- `viewport-fit=cover` 适配刘海屏
|
- `viewport-fit=cover` 适配刘海屏
|
||||||
|
|
||||||
|
### 盲盒房间删除 / 退出
|
||||||
|
- 房间创建者可删除房间(级联清理成员 & 想法)
|
||||||
|
- 非创建者可退出房间(从成员列表移除自己)
|
||||||
|
|
||||||
### 首次体验引导优化
|
### 首次体验引导优化
|
||||||
- ~~极速救场完成一轮后引导注册("注册保存记录")~~(已完成,匹配成功页展示注册卡片,注册后自动保存记录)
|
- ~~极速救场完成一轮后引导注册("注册保存记录")~~(已完成,匹配成功页展示注册卡片,注册后自动保存记录)
|
||||||
- 盲盒模式先展示 demo / 动画,让用户看到价值再引导注册
|
- 盲盒模式先展示 demo / 动画,让用户看到价值再引导注册
|
||||||
@@ -78,11 +82,6 @@
|
|||||||
- 盲盒投放数量成就
|
- 盲盒投放数量成就
|
||||||
- 在个人中心展示,增加用户粘性
|
- 在个人中心展示,增加用户粘性
|
||||||
|
|
||||||
### 盲盒房间生命周期
|
|
||||||
- 闲置 30 天自动归档
|
|
||||||
- 支持删除 / 退出房间
|
|
||||||
- 房间设置页(修改名称、管理成员、清空想法池)
|
|
||||||
|
|
||||||
### 浅色模式
|
### 浅色模式
|
||||||
- 当前暗色主题是唯一选项
|
- 当前暗色主题是唯一选项
|
||||||
- 白天户外使用体验差
|
- 白天户外使用体验差
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
import { errorResponse, getRoomByCode } from "@/lib/blindbox";
|
import { errorResponse, getRoomByCode } from "@/lib/blindbox";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
@@ -28,3 +29,35 @@ export async function GET(
|
|||||||
return errorResponse("获取房间信息失败", 500);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
Share2,
|
Share2,
|
||||||
LogIn,
|
LogIn,
|
||||||
Copy,
|
Copy,
|
||||||
|
Trash2,
|
||||||
|
LogOut,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||||
@@ -60,6 +62,8 @@ export default function BlindboxRoomPage() {
|
|||||||
const [showInvite, setShowInvite] = useState(false);
|
const [showInvite, setShowInvite] = useState(false);
|
||||||
const [showShareCard, setShowShareCard] = useState(false);
|
const [showShareCard, setShowShareCard] = useState(false);
|
||||||
const [toast, setToast] = useState("");
|
const [toast, setToast] = useState("");
|
||||||
|
const [confirmLeave, setConfirmLeave] = useState(false);
|
||||||
|
const [leaving, setLeaving] = useState(false);
|
||||||
|
|
||||||
const boxControls = useAnimation();
|
const boxControls = useAnimation();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -250,6 +254,36 @@ export default function BlindboxRoomPage() {
|
|||||||
handleCopyCode();
|
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) {
|
if (pageLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-dvh items-center justify-center bg-background">
|
<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 */}
|
{/* Toast */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{toast && (
|
{toast && (
|
||||||
|
|||||||
Reference in New Issue
Block a user