"use client"; import { useState, useCallback } from "react"; import { useSession } from "next-auth/react"; import toast from "react-hot-toast"; import { useVoteStore, selectRemaining, DAILY_VOTE_QUOTA, } from "@/lib/store"; import { useLoginModalStore } from "@/lib/login-modal-store"; import type { Artist } from "@/types/artist"; interface UseVoteActionResult { /** 当前投票目标艺人(null 时弹窗关闭) */ target: Artist | null; /** 今日剩余票数 */ remaining: number; /** 每日总额度(常量,供 UI 文案展示) */ dailyQuota: number; /** 触发投票(自动检查登录态 + 额度) */ openVote: (artist: Artist) => void; /** 关闭投票弹窗 */ closeVote: () => void; /** 确认投票(已登录态下调用) */ confirmVote: (artist: Artist, count: number) => Promise; } /** * 投票交互统一入口。 * * 规则: * - 每用户每日总额度 = 10 票,跨艺人共享。无单艺人上限。 * - 未登录 → toast 提示并跳登录页 * - 已登录但当日票数已用完 → toast 提示,不打开弹窗 * - 弹窗确认后:本地 store 立即扣减 + 调用后端 API(fire-and-forget) */ export function useVoteAction(): UseVoteActionResult { const { status } = useSession(); const recordVote = useVoteStore((s) => s.vote); const remaining = useVoteStore(selectRemaining); const openLogin = useLoginModalStore((s) => s.show); const [target, setTarget] = useState(null); const openVote = useCallback( (artist: Artist) => { if (status === "loading") return; if (status === "unauthenticated") { toast("请先登录后再为偶像投票"); setTimeout(openLogin, 350); return; } if (remaining <= 0) { toast("今日票数已用完,明天再来吧"); return; } setTarget(artist); }, [status, openLogin, remaining], ); const closeVote = useCallback(() => setTarget(null), []); const confirmVote = useCallback( async (artist: Artist, count: number) => { // 1. 本地 store 立即扣减(包含额度校验) const success = recordVote(artist.id, count); if (!success) { toast.error("今日票数不足"); setTarget(null); return; } toast.success(`已为 ${artist.name} 投出 ${count} 票`); setTarget(null); // 2. 后台 fire-and-forget 调用真实 API(5 秒超时,失败静默忽略) const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 5000); fetch("/api/vote", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ artistId: artist.id, count }), signal: ctrl.signal, }) .catch(() => {}) .finally(() => clearTimeout(timer)); }, [recordVote], ); return { target, remaining, dailyQuota: DAILY_VOTE_QUOTA, openVote, closeVote, confirmVote, }; }