refactor: 提取 useToast hook + Toast 组件,消除 4 处重复的通知逻辑
将 state + setTimeout 自动消失逻辑封装为 useToast hook, Toast UI 统一为组件支持 top/bottom 两种位置,净减约 12 行。
This commit is contained in:
@@ -23,6 +23,8 @@ import {
|
||||
import confetti from "canvas-confetti";
|
||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||
import ShareCardModal from "@/components/ShareCardModal";
|
||||
import Toast from "@/components/Toast";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { BlindboxRoomSkeleton } from "@/components/Skeleton";
|
||||
import type { UserProfile } from "@/types";
|
||||
|
||||
@@ -182,7 +184,7 @@ export default function BlindboxRoomPage() {
|
||||
const [error, setError] = useState("");
|
||||
const [showInvite, setShowInvite] = useState(false);
|
||||
const [showShareCard, setShowShareCard] = useState(false);
|
||||
const [toast, setToast] = useState("");
|
||||
const toast = useToast();
|
||||
const [confirmLeave, setConfirmLeave] = useState(false);
|
||||
const [leaving, setLeaving] = useState(false);
|
||||
|
||||
@@ -314,8 +316,7 @@ export default function BlindboxRoomPage() {
|
||||
}
|
||||
setMyIdeas((prev) => prev.map((i) => (i.id === ideaId ? { ...i, content: trimmed } : i)));
|
||||
} catch (e) {
|
||||
setToast(e instanceof Error ? e.message : "编辑失败");
|
||||
setTimeout(() => setToast(""), 2200);
|
||||
toast.show(e instanceof Error ? e.message : "编辑失败");
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
@@ -334,8 +335,7 @@ export default function BlindboxRoomPage() {
|
||||
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);
|
||||
toast.show(e instanceof Error ? e.message : "删除失败");
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
@@ -395,8 +395,7 @@ export default function BlindboxRoomPage() {
|
||||
if (!room) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(room.code);
|
||||
setToast("房间号已复制");
|
||||
setTimeout(() => setToast(""), 2000);
|
||||
toast.show("房间号已复制");
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
@@ -441,8 +440,7 @@ export default function BlindboxRoomPage() {
|
||||
}
|
||||
router.replace("/blindbox");
|
||||
} catch (e) {
|
||||
setToast(e instanceof Error ? e.message : "操作失败");
|
||||
setTimeout(() => setToast(""), 2200);
|
||||
toast.show(e instanceof Error ? e.message : "操作失败");
|
||||
setConfirmLeave(false);
|
||||
} finally {
|
||||
setLeaving(false);
|
||||
@@ -817,10 +815,7 @@ export default function BlindboxRoomPage() {
|
||||
drawer: revealedIdea.drawnBy ?? undefined,
|
||||
roomName: room.name,
|
||||
}}
|
||||
onToast={(msg) => {
|
||||
setToast(msg);
|
||||
setTimeout(() => setToast(""), 2200);
|
||||
}}
|
||||
onToast={toast.show}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -855,19 +850,7 @@ export default function BlindboxRoomPage() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Toast */}
|
||||
<AnimatePresence>
|
||||
{toast && (
|
||||
<motion.div
|
||||
className="fixed bottom-8 left-1/2 -translate-x-1/2 rounded-full bg-surface px-4 py-2 text-xs font-semibold text-secondary shadow-xl ring-1 ring-border"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
{toast}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Toast message={toast.message} position="bottom" />
|
||||
|
||||
<div className="h-8 shrink-0" />
|
||||
</div>
|
||||
|
||||
+10
-25
@@ -24,6 +24,8 @@ import {
|
||||
Heart,
|
||||
} from "lucide-react";
|
||||
import EmptyState from "@/components/EmptyState";
|
||||
import Toast from "@/components/Toast";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import RestaurantImage from "@/components/RestaurantImage";
|
||||
import { ProfileCardSkeleton, RecordItemSkeleton } from "@/components/Skeleton";
|
||||
import { getUserId, getCachedProfile, setCachedProfile, setCachedPreferences, logout } from "@/lib/userId";
|
||||
@@ -68,12 +70,7 @@ export default function ProfilePage() {
|
||||
|
||||
const [showHistory, setShowHistory] = useState(true);
|
||||
const [showFavorites, setShowFavorites] = useState(true);
|
||||
const [toast, setToast] = useState("");
|
||||
|
||||
const showToast = useCallback((msg: string) => {
|
||||
setToast(msg);
|
||||
setTimeout(() => setToast(""), 2200);
|
||||
}, []);
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const cached = getCachedProfile();
|
||||
@@ -141,7 +138,7 @@ export default function ProfilePage() {
|
||||
setProfile((prev) => prev ? { ...prev, username: trimmed } : prev);
|
||||
setCachedProfile({ id: userId, username: trimmed, avatar: profile!.avatar });
|
||||
setEditingUsername(false);
|
||||
showToast("用户名已更新");
|
||||
toast.show("用户名已更新");
|
||||
} else {
|
||||
setUsernameMsg(data.error ?? "更新失败");
|
||||
}
|
||||
@@ -180,7 +177,7 @@ export default function ProfilePage() {
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
showToast("密码已更新");
|
||||
toast.show("密码已更新");
|
||||
} else {
|
||||
setPasswordMsg(data.error ?? "更新失败");
|
||||
}
|
||||
@@ -202,10 +199,10 @@ export default function ProfilePage() {
|
||||
setProfile((prev) => prev ? { ...prev, avatar: emoji } : prev);
|
||||
setCachedProfile({ id: userId, username: profile!.username, avatar: emoji });
|
||||
setEditingAvatar(false);
|
||||
showToast("头像已更新");
|
||||
toast.show("头像已更新");
|
||||
}
|
||||
} catch {
|
||||
showToast("更新失败");
|
||||
toast.show("更新失败");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -244,9 +241,9 @@ export default function ProfilePage() {
|
||||
body: JSON.stringify({ userId, favoriteId: favId }),
|
||||
});
|
||||
setFavorites((f) => f.filter((x) => x.id !== favId));
|
||||
showToast("已取消收藏");
|
||||
toast.show("已取消收藏");
|
||||
} catch {
|
||||
showToast("操作失败");
|
||||
toast.show("操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -714,19 +711,7 @@ export default function ProfilePage() {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{toast && (
|
||||
<motion.div
|
||||
className="fixed left-1/2 top-10 z-60 -translate-x-1/2 rounded-xl bg-elevated px-4 py-2.5 text-xs font-medium text-heading shadow-lg ring-1 ring-subtle"
|
||||
initial={{ opacity: 0, y: -12, x: "-50%" }}
|
||||
animate={{ opacity: 1, y: 0, x: "-50%" }}
|
||||
exit={{ opacity: 0, y: -12, x: "-50%" }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||
>
|
||||
{toast}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Toast message={toast.message} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user