"use client"; import { useEffect, useState, useCallback } from "react"; import { createPortal } from "react-dom"; import { AnimatePresence, motion } from "framer-motion"; import { X, Heart } from "lucide-react"; import type { Artist } from "@/types/artist"; import { cn } from "@/lib/cn"; import Button from "./ui/Button"; import ArtistPortrait from "./cards/ArtistPortrait"; type VoteOption = number | "ALL"; interface VoteModalProps { /** 当前要投票的艺人,传 null 关闭弹窗 */ artist: Artist | null; /** 今日剩余票数(ALL 即投出该数值) */ remaining: number; /** 每日总额度(用于副文案展示) */ dailyQuota: number; /** 关闭弹窗 */ onClose: () => void; /** 确认投票(count 为最终实际投票数,ALL 会被解析为 remaining) */ onConfirm: (artist: Artist, count: number) => void | Promise; } const VOTE_OPTIONS: VoteOption[] = [1, 3, 5, "ALL"]; function defaultOption(remaining: number): VoteOption { if (remaining >= 3) return 3; if (remaining >= 1) return remaining as VoteOption; return "ALL"; } function resolveCount(opt: VoteOption, remaining: number): number { return opt === "ALL" ? remaining : opt; } export default function VoteModal({ artist, remaining, dailyQuota, onClose, onConfirm, }: VoteModalProps) { const open = artist != null; const [selected, setSelected] = useState(defaultOption(remaining)); const [loading, setLoading] = useState(false); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); // 打开时重置默认选择 useEffect(() => { if (open) { setSelected(defaultOption(remaining)); setLoading(false); } }, [open, remaining]); // ESC 关闭 + body 锁滚 useEffect(() => { if (!open) return; const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", handler); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", handler); document.body.style.overflow = prev; }; }, [open, onClose]); const actualCount = resolveCount(selected, remaining); const canSubmit = remaining > 0 && actualCount > 0 && actualCount <= remaining; const handleConfirm = useCallback(async () => { if (!artist || loading || !canSubmit) return; setLoading(true); try { await onConfirm(artist, actualCount); } finally { setLoading(false); } }, [artist, actualCount, canSubmit, loading, onConfirm]); if (!mounted) return null; return createPortal( {open && artist && ( {/* 遮罩 */} {/* 头像 */}
{/* 标题 */}
为 {artist.name} 投票
No.{artist.no} · Current Rank #{artist.rank}
{/* 剩余票数提示 */}
选择投票数: 今日剩余 {remaining} / {dailyQuota}
{/* 票数选择 */}
{VOTE_OPTIONS.map((opt) => { const active = selected === opt; const optValue = resolveCount(opt, remaining); const disabled = remaining === 0 || optValue === 0 || optValue > remaining || (opt === "ALL" && remaining === 0); return ( ); })}
{/* 确认按钮 */}
)}
, document.body, ); }