Files
no-whatever/src/hooks/useBlindboxDraw.ts
T
kurihada 6bb0e65d4c refactor(P1): 5 项代码质量改进 — 消除重复、拆分巨型组件、统一基础设施
Task 4: 统一 amap.ts 为完整 API 客户端
- 扩展 amap.ts 为统一客户端(amapFetch 8s 超时 + 错误处理)
- 导出 searchPlaceText/searchPlaceAround/getInputTips/reverseGeocode/getTransitDirection
- 精简 4 个 location route 为单行调用,blindboxPlanGen 删除 ~80 行内联 API 代码

Task 2: 抽取 ShareCardShell 消除三兄弟重复
- 新建 ShareCardShell.tsx 共享外框/背景/品牌头/QR 底部
- RestaurantShareCard 406→268 行,BlindboxShareCard 341→173 行,BlindboxPlanShareCard 277→159 行

Task 3: 拆分 BlindboxPlan.tsx (742→371 行)
- 提取 planUtils.ts (guessCategory + formatDuration)
- 提取 PoiSearchField / SortablePlanItem / PlanItemEditModal 三个独立组件

Task 1: 拆分 blindbox/[code]/page.tsx 上帝组件 (1300→509 行)
- 提取 useBlindboxRoom / useBlindboxIdeas / useBlindboxPlan / useBlindboxDraw 四个 hooks
- 提取 BlindboxPoolPhase / BlindboxRevealPhase 两个子组件
- 主页面仅保留 phase 协调 + hook 组装 + 子组件渲染

Task 5: 统一 SWR 数据获取层
- 新建 fetcher.ts (FetchError 携带 status,401 不重试)
- 新建 useBlindboxRooms / useAchievements / useFavorites SWR hooks
- useRoomPolling 改用共享 fetcher
- blindbox 大厅/成就/个人中心页面删除手写 fetch 样板代码
- JWT 过期时自动弹出登录框而非反复重试
2026-03-02 18:05:06 +08:00

96 lines
3.1 KiB
TypeScript

"use client";
import { useState, useRef, useCallback } from "react";
import { useAnimation } from "framer-motion";
import confetti from "canvas-confetti";
import type { DrawnIdea } from "@/components/BlindboxDrawnHistory";
import type { RoomInfo } from "@/hooks/useBlindboxRoom";
import type { UserProfile } from "@/types";
type Phase = "pool" | "shaking" | "reveal" | "time_select" | "planning" | "plan_reveal";
export function useBlindboxDraw(
room: RoomInfo | null,
profile: UserProfile | null,
poolCount: number,
setPoolCount: React.Dispatch<React.SetStateAction<number>>,
setDrawnHistory: React.Dispatch<React.SetStateAction<DrawnIdea[]>>,
setError: (e: string) => void,
setPhase: (p: Phase) => void,
) {
const [revealedIdea, setRevealedIdea] = useState<DrawnIdea | null>(null);
const [showShareCard, setShowShareCard] = useState(false);
const boxControls = useAnimation();
const confettiAliveRef = useRef(false);
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
const fireConfetti = useCallback(() => {
const colors = ["#a855f7", "#6366f1", "#ec4899", "#f59e0b", "#10b981"];
confetti({ particleCount: 100, spread: 120, origin: { y: 0.4 }, colors, startVelocity: 45, ticks: 250 });
confettiAliveRef.current = true;
const end = Date.now() + 3000;
const frame = () => {
if (Date.now() > end || !confettiAliveRef.current) return;
confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0, y: 0.6 }, colors, startVelocity: 35, ticks: 150 });
confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1, y: 0.6 }, colors, startVelocity: 35, ticks: 150 });
requestAnimationFrame(frame);
};
timersRef.current.push(setTimeout(frame, 200));
}, []);
const handleDraw = async () => {
if (poolCount === 0 || !profile || !room) {
setError("盒子是空的,先往里面塞点想法吧!");
return;
}
setPhase("shaking");
setError("");
await boxControls.start({
rotate: [0, -8, 8, -10, 10, -12, 12, -8, 8, -4, 4, 0],
scale: [1, 1.05, 0.95, 1.08, 0.92, 1.1, 0.9, 1.05, 0.95, 1],
transition: { duration: 2.5, ease: "easeInOut" },
});
try {
const res = await fetch("/api/blindbox/draw", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomId: room.id, userId: profile.id }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "抽取失败");
}
const idea = await res.json();
setRevealedIdea(idea);
setPhase("reveal");
setPoolCount((c) => Math.max(0, c - 1));
setDrawnHistory((prev) => [idea, ...prev]);
fireConfetti();
} catch (e) {
setError(e instanceof Error ? e.message : "抽取失败");
setPhase("pool");
}
};
const handleContinue = useCallback(() => {
setPhase("pool");
setRevealedIdea(null);
setShowShareCard(false);
}, [setPhase]);
return {
revealedIdea,
showShareCard,
setShowShareCard,
boxControls,
fireConfetti,
handleDraw,
handleContinue,
};
}