import { create } from "zustand"; import { persist } from "zustand/middleware"; import { ARTISTS } from "./mock-data"; import type { Artist } from "@/types/artist"; /** * 每用户终身投票总额度 —— 一人 12 票,每位艺人最多 1 票, * 投完即结束,不限时,不可撤销。 */ export const TOTAL_VOTE_QUOTA = 12; /** 派生类型:我应援的艺人(新规则下每位仅 1 票,不再带 votedCount) */ export interface MySupport { artist: Artist; } interface VoteStore { /** 当前所有艺人(含动态票数 / 实时排名) */ artists: Artist[]; /** * 我已投票的艺人 ID 列表(顺序 = 投票顺序)。 * 用数组而不是 Set:zustand persist 默认 storage 是 JSON,Set 无法直接序列化。 * 数组按投票顺序保留,既能 .includes() 判重,也能在 /me 页面显示"我的投票"时按时间排序。 */ votedArtists: string[]; /** * 给艺人投票。 * - 已投过该艺人 → 返回 { ok: false, reason: "already" } * - 已用满 12 票 → 返回 { ok: false, reason: "exhausted" } * - 成功 → 返回 { ok: true } */ vote: (artistId: string) => { ok: boolean; reason?: "already" | "exhausted" }; /** 重置(开发时用 / 测试用) */ reset: () => void; } /** 票数倒序 + 编号升序兜底,确保 0 票时也有稳定排名 */ function rank(list: Artist[]): Artist[] { return [...list] .sort((a, b) => b.votes - a.votes || a.no.localeCompare(b.no)) .map((a, i) => ({ ...a, rank: i + 1 })); } const INITIAL_ARTISTS = rank(ARTISTS); export const useVoteStore = create()( persist( (set, get) => ({ artists: INITIAL_ARTISTS, votedArtists: [], vote: (artistId) => { const state = get(); if (state.votedArtists.includes(artistId)) { return { ok: false, reason: "already" }; } if (state.votedArtists.length >= TOTAL_VOTE_QUOTA) { return { ok: false, reason: "exhausted" }; } const updated = state.artists.map((a) => a.id === artistId ? { ...a, votes: a.votes + 1 } : a, ); set({ artists: rank(updated), votedArtists: [...state.votedArtists, artistId], }); return { ok: true }; }, reset: () => set({ artists: INITIAL_ARTISTS, votedArtists: [], }), }), { name: "cyber-star-vote", // 仅持久化 votedArtists —— artists 票数/排名是派生数据, // 刷新后重新从初始数据 + votedArtists 重建。 // 注意:当前 mock 阶段 artists 只反映本地投票,不同步服务端 —— 等后端接入再调整。 partialize: (state) => ({ votedArtists: state.votedArtists }), // rehydrate 时把 votedArtists 数据"回放"到 artists 票数上,保持视图一致 onRehydrateStorage: () => (state) => { if (!state) return; const counts = new Map(); for (const id of state.votedArtists) { counts.set(id, (counts.get(id) ?? 0) + 1); } const rebuilt = INITIAL_ARTISTS.map((a) => ({ ...a, votes: a.votes + (counts.get(a.id) ?? 0), })); state.artists = rank(rebuilt); }, }, ), ); /** 选择器:按 ID 获取最新艺人 */ export function selectArtist(id: string) { return (s: VoteStore) => s.artists.find((a) => a.id === id); } /** 选择器:当前剩余票数 = 12 - 已投艺人数 */ export function selectRemaining(s: VoteStore): number { return Math.max(0, TOTAL_VOTE_QUOTA - s.votedArtists.length); } /** 选择器:是否已投过指定艺人(高阶,在组件里用 useVoteStore(selectHasVoted(id))) */ export function selectHasVoted(id: string) { return (s: VoteStore) => s.votedArtists.includes(id); } /** 选择器:12 票是否已全部投完 */ export function selectIsExhausted(s: VoteStore): boolean { return s.votedArtists.length >= TOTAL_VOTE_QUOTA; } /** * 派生函数:"我的应援"列表 —— 按投票顺序(最早投的在前)。 * * ⚠️ 不要直接 `useVoteStore(selectMySupports)`:它每次都返回新数组, * 会触发 React 19 的 "getSnapshot should be cached" 报错。 * 正确用法:在组件里 useMemo 派生(参考 MeContent.tsx)。 */ export function selectMySupports(s: VoteStore): MySupport[] { const list: MySupport[] = []; for (const id of s.votedArtists) { const artist = s.artists.find((a) => a.id === id); if (artist) list.push({ artist }); } return list; } /** 选择器:我支持的艺人数 = 投票数 */ export function selectMySupportingCount(s: VoteStore): number { return s.votedArtists.length; }