diff --git a/src/app/api/blindbox/plan/route.ts b/src/app/api/blindbox/plan/route.ts index b0625fb..5b9bc3b 100644 --- a/src/app/api/blindbox/plan/route.ts +++ b/src/app/api/blindbox/plan/route.ts @@ -190,16 +190,18 @@ export const POST = apiHandler(async (req) => { ] : [at]; - // Select ideas per day, avoiding duplicates across days when possible + // Select ideas per day — skip extra days when ideas run out const dayIdeas: TaggedIdea[][] = []; const usedIds = new Set(); for (const dayConfig of dayConfigs) { const remaining = taggedIdeas.filter((i) => !usedIds.has(i.id)); - const pool = remaining.length >= 2 ? remaining : taggedIdeas; - const selected = selectIdeasForSlots(pool, dayConfig.endHour - dayConfig.startHour); + if (remaining.length < 2) break; + const selected = selectIdeasForSlots(remaining, dayConfig.endHour - dayConfig.startHour); for (const idea of selected) usedIds.add(idea.id); dayIdeas.push(selected); } + // Trim to actual days generated (may be fewer than requested for "整个周末") + const actualDayConfigs = dayConfigs.slice(0, dayIdeas.length); const allSelected = dayIdeas.flat(); if (allSelected.length === 0) { @@ -260,7 +262,7 @@ export const POST = apiHandler(async (req) => { // Generate schedule for each day (parallel AI calls) const schedules = await Promise.all( - dayConfigs.map((dayConfig, idx) => { + actualDayConfigs.map((dayConfig, idx) => { const ideas = dayIdeas[idx]; const ctx: ScheduleContext = { ideas: ideas.map((i) => ({ @@ -282,7 +284,7 @@ export const POST = apiHandler(async (req) => { const days = schedules .map((schedule, idx) => schedule - ? { date: dayConfigs[idx].date, items: schedule.items, summary: schedule.summary } + ? { date: actualDayConfigs[idx].date, items: schedule.items, summary: schedule.summary } : null, ) .filter((d) => d !== null); @@ -308,3 +310,47 @@ export const POST = apiHandler(async (req) => { createdAt: plan.createdAt, }); }); + +export const PATCH = apiHandler(async (req) => { + const { planId, userId } = await req.json(); + requireUserId(userId); + if (!planId) throw new ApiError("planId 不能为空"); + + const plan = await prisma.weekendPlan.findUnique({ where: { id: planId } }); + if (!plan) throw new ApiError("计划不存在", 404); + if (plan.userId !== userId) throw new ApiError("只能接受自己的计划", 403); + + await prisma.weekendPlan.update({ + where: { id: planId }, + data: { status: "accepted" }, + }); + + return NextResponse.json({ ok: true }); +}); + +export const GET = apiHandler(async (req) => { + const { searchParams } = new URL(req.url); + const roomId = searchParams.get("roomId"); + const userId = searchParams.get("userId"); + + requireUserId(userId); + if (!roomId) throw new ApiError("roomId 不能为空"); + + const plan = await prisma.weekendPlan.findFirst({ + where: { roomId, userId: userId!, status: "accepted" }, + orderBy: { createdAt: "desc" }, + select: { id: true, planData: true, createdAt: true }, + }); + + if (!plan) return NextResponse.json({ plan: null }); + + const parsed = JSON.parse(plan.planData); + + return NextResponse.json({ + plan: { + id: plan.id, + days: parsed.days, + createdAt: plan.createdAt, + }, + }); +}); diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index 740aba3..f8046b0 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -71,6 +71,7 @@ export default function BlindboxRoomPage() { const [confirmLeave, setConfirmLeave] = useState(false); const [leaving, setLeaving] = useState(false); const [locating, setLocating] = useState(false); + const [planId, setPlanId] = useState(null); const [planDays, setPlanDays] = useState([]); const [planAccepted, setPlanAccepted] = useState(false); const [generating, setGenerating] = useState(false); @@ -136,9 +137,28 @@ export default function BlindboxRoomPage() { } catch { /* ignore */ } }, [room]); + const fetchAcceptedPlan = useCallback(async () => { + const p = getCachedProfile(); + if (!room || !p) return; + try { + const res = await fetch(`/api/blindbox/plan?roomId=${room.id}&userId=${p.id}`); + if (!res.ok) return; + const data = await res.json(); + if (data.plan) { + setPlanId(data.plan.id); + setPlanDays(data.plan.days); + setPlanAccepted(true); + setPhase("plan_reveal"); + } + } catch { /* ignore */ } + }, [room]); + useEffect(() => { - if (isMember && room) fetchIdeas(); - }, [isMember, room, fetchIdeas]); + if (isMember && room) { + fetchIdeas(); + fetchAcceptedPlan(); + } + }, [isMember, room, fetchIdeas, fetchAcceptedPlan]); useEffect(() => { if (isMember && inputRef.current) { @@ -212,6 +232,7 @@ export default function BlindboxRoomPage() { throw new Error(data.error || "生成失败"); } const data = await res.json(); + setPlanId(data.id); setPlanDays(data.days); setPlanAccepted(false); setPhase("plan_reveal"); @@ -782,9 +803,18 @@ export default function BlindboxRoomPage() { days={planDays} accepted={planAccepted} regenerating={generating} - onAccept={() => { + onAccept={async () => { setPlanAccepted(true); fireConfetti(); + if (planId && profile) { + try { + await fetch("/api/blindbox/plan", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ planId, userId: profile.id }), + }); + } catch { /* best-effort */ } + } }} onRegenerate={() => { setPhase("time_select"); @@ -792,6 +822,7 @@ export default function BlindboxRoomPage() { onShare={() => setShowPlanShareCard(true)} onBack={() => { setPhase("pool"); + setPlanId(null); setPlanDays([]); setPlanAccepted(false); }}