From ac8cb8c63548348b1eb263e59c6933a2ad5da8b8 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 16:55:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=B2=E7=9B=92=E6=83=B3=E6=B3=95?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BC=96=E8=BE=91=E5=92=8C=E5=88=A0=E9=99=A4?= =?UTF-8?q?=EF=BC=8C=E5=B1=95=E7=A4=BA=E7=94=A8=E6=88=B7=E5=B7=B2=E6=8A=95?= =?UTF-8?q?=E5=85=A5=E7=9A=84=E6=83=B3=E6=B3=95=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/blindbox/route.ts | 58 ++++++++++- src/app/blindbox/[code]/page.tsx | 173 +++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 2 deletions(-) diff --git a/src/app/api/blindbox/route.ts b/src/app/api/blindbox/route.ts index 9c9ef3c..31acc9e 100644 --- a/src/app/api/blindbox/route.ts +++ b/src/app/api/blindbox/route.ts @@ -55,10 +55,15 @@ export async function GET(req: NextRequest) { } try { - const [poolCount, drawn] = await Promise.all([ + const [poolCount, myIdeas, drawn] = await Promise.all([ prisma.blindBoxIdea.count({ where: { roomId, status: "in_pool" }, }), + prisma.blindBoxIdea.findMany({ + where: { roomId, userId, status: "in_pool" }, + orderBy: { createdAt: "desc" }, + select: { id: true, content: true, createdAt: true }, + }), prisma.blindBoxIdea.findMany({ where: { roomId, status: "drawn" }, orderBy: { createdAt: "desc" }, @@ -69,8 +74,57 @@ export async function GET(req: NextRequest) { }), ]); - return NextResponse.json({ poolCount, drawn }); + return NextResponse.json({ poolCount, myIdeas, drawn }); } catch { return errorResponse("查询失败", 500); } } + +export async function PUT(req: NextRequest) { + try { + const { ideaId, userId, content } = await req.json(); + + if (!userId) return errorResponse("请先登录", 401); + if (!ideaId) return errorResponse("缺少 ideaId", 400); + if (!content || typeof content !== "string" || content.trim().length === 0) { + return errorResponse("内容不能为空", 400); + } + if (content.trim().length > 200) { + return errorResponse("内容不能超过 200 字", 400); + } + + const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); + if (!idea) return errorResponse("想法不存在", 404); + if (idea.userId !== userId) return errorResponse("只能编辑自己的想法", 403); + if (idea.status !== "in_pool") return errorResponse("已抽中的想法不能编辑", 400); + + const updated = await prisma.blindBoxIdea.update({ + where: { id: ideaId }, + data: { content: content.trim() }, + }); + + return NextResponse.json({ id: updated.id, content: updated.content }); + } catch { + return errorResponse("编辑失败", 500); + } +} + +export async function DELETE(req: NextRequest) { + try { + const { ideaId, userId } = await req.json(); + + if (!userId) return errorResponse("请先登录", 401); + if (!ideaId) return errorResponse("缺少 ideaId", 400); + + const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); + if (!idea) return errorResponse("想法不存在", 404); + if (idea.userId !== userId) return errorResponse("只能删除自己的想法", 403); + if (idea.status !== "in_pool") return errorResponse("已抽中的想法不能删除", 400); + + await prisma.blindBoxIdea.delete({ where: { id: ideaId } }); + + return NextResponse.json({ deleted: true }); + } catch { + return errorResponse("删除失败", 500); + } +} diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index 25e5f93..33819b8 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -16,6 +16,9 @@ import { Copy, Trash2, LogOut, + Pencil, + Check, + X, } from "lucide-react"; import confetti from "canvas-confetti"; import { getCachedProfile, isRegistered } from "@/lib/userId"; @@ -32,6 +35,12 @@ interface RoomInfo { members: { id: string; username: string; avatar: string }[]; } +interface MyIdea { + id: string; + content: string; + createdAt: string; +} + interface DrawnIdea { id: string; content: string; @@ -42,6 +51,116 @@ interface DrawnIdea { type Phase = "pool" | "shaking" | "reveal"; +function MyIdeaItem({ + idea, + onEdit, + onDelete, +}: { + idea: MyIdea; + onEdit: (id: string, content: string) => Promise; + onDelete: (id: string) => Promise; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(idea.content); + const [saving, setSaving] = useState(false); + + const handleSave = async () => { + if (!draft.trim() || saving) return; + setSaving(true); + await onEdit(idea.id, draft); + setSaving(false); + setEditing(false); + }; + + return ( + + {editing ? ( + <> + setDraft(e.target.value.slice(0, 200))} + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); if (e.key === "Escape") { setEditing(false); setDraft(idea.content); } }} + maxLength={200} + autoFocus + className="h-8 min-w-0 flex-1 rounded-lg bg-elevated px-2.5 text-sm text-foreground outline-none ring-1 ring-border focus:ring-2 focus:ring-purple-600/50" + /> + + + + ) : ( + <> + 💡 +

{idea.content}

+ + + + )} +
+ ); +} + +function MyIdeasSection({ + ideas, + onEdit, + onDelete, +}: { + ideas: MyIdea[]; + onEdit: (id: string, content: string) => Promise; + onDelete: (id: string) => Promise; +}) { + return ( + +
+ +

+ 我投入的想法({ideas.length}) +

+
+
+
+ + {ideas.map((idea) => ( + + ))} + +
+ + ); +} + export default function BlindboxRoomPage() { const { code } = useParams<{ code: string }>(); const router = useRouter(); @@ -55,6 +174,7 @@ export default function BlindboxRoomPage() { const [input, setInput] = useState(""); const [submitting, setSubmitting] = useState(false); const [poolCount, setPoolCount] = useState(0); + const [myIdeas, setMyIdeas] = useState([]); const [drawnHistory, setDrawnHistory] = useState([]); const [phase, setPhase] = useState("pool"); const [revealedIdea, setRevealedIdea] = useState(null); @@ -112,6 +232,7 @@ export default function BlindboxRoomPage() { if (res.ok) { const data = await res.json(); setPoolCount(data.poolCount ?? 0); + setMyIdeas(data.myIdeas ?? []); setDrawnHistory(data.drawn ?? []); } } catch { /* ignore */ } @@ -159,8 +280,10 @@ export default function BlindboxRoomPage() { const data = await res.json(); throw new Error(data.error || "提交失败"); } + const { id } = await res.json(); setInput(""); setPoolCount((c) => c + 1); + setMyIdeas((prev) => [{ id, content: text, createdAt: new Date().toISOString() }, ...prev]); setSubmitFlash(true); setTimeout(() => setSubmitFlash(false), 600); boxControls.start({ @@ -175,6 +298,47 @@ export default function BlindboxRoomPage() { } }; + const handleEditIdea = useCallback(async (ideaId: string, newContent: string) => { + if (!profile) return; + const trimmed = newContent.trim(); + if (!trimmed || trimmed.length > 200) return; + try { + const res = await fetch("/api/blindbox", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ideaId, userId: profile.id, content: trimmed }), + }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "编辑失败"); + } + setMyIdeas((prev) => prev.map((i) => (i.id === ideaId ? { ...i, content: trimmed } : i))); + } catch (e) { + setToast(e instanceof Error ? e.message : "编辑失败"); + setTimeout(() => setToast(""), 2200); + } + }, [profile]); + + const handleDeleteIdea = useCallback(async (ideaId: string) => { + if (!profile) return; + try { + const res = await fetch("/api/blindbox", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ideaId, userId: profile.id }), + }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "删除失败"); + } + setMyIdeas((prev) => prev.filter((i) => i.id !== ideaId)); + setPoolCount((c) => Math.max(0, c - 1)); + } catch (e) { + setToast(e instanceof Error ? e.message : "删除失败"); + setTimeout(() => setToast(""), 2200); + } + }, [profile]); + const handleDraw = async () => { if (poolCount === 0 || !profile || !room) { setError("盒子是空的,先往里面塞点想法吧!"); @@ -576,6 +740,15 @@ export default function BlindboxRoomPage() { )} + {/* My ideas in pool */} + {myIdeas.length > 0 && phase === "pool" && ( + + )} + {/* History */} {drawnHistory.length > 0 && phase !== "shaking" && (