Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
前端: - store 改为 votedArtists[] + zustand persist - VoteModal 删除 1/3/5/ALL 选择器,改三态(待投/已投/满额) - 卡片/排行/详情页加 hasVoted 状态 + ✓ 角标 - Hero 右上角 Countdown 替换为 HeroVoteProgress(12 格点亮进度) - /me 改为终身额度叙事(QuotaCard / StatsGrid / MyFanSupport) 后端: - votes 表加 @@unique([userId, artistId])(已 apply 到生产 RDS) - /api/vote 重写:12 票上限 + P2002 ALREADY_VOTED + P2003 NOT_FOUND 兜底 - /api/me 新增 votedArtists[] + voteQuota,移除 dailyQuota - 新增 ERR.ALREADY_VOTED 错误码 测试: - DB 层 5/5 + E2E 18/18 通过(scripts/e2e-vote-flow.sh) - 修复 P2003 FK 违反未识别的 bug 详情见 docs/todo/voting-refactor-完成报告.md 与 voting-refactor-backend-完成报告.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { signOut } from "next-auth/react";
|
|
import toast from "react-hot-toast";
|
|
import UserHeader from "@/components/me/UserHeader";
|
|
import QuotaCard from "@/components/me/QuotaCard";
|
|
import StatsGrid from "@/components/me/StatsGrid";
|
|
import MyFanSupport from "@/components/me/MyFanSupport";
|
|
import {
|
|
useVoteStore,
|
|
selectRemaining,
|
|
TOTAL_VOTE_QUOTA,
|
|
type MySupport,
|
|
} from "@/lib/store";
|
|
|
|
interface MeContentProps {
|
|
session: {
|
|
id: string;
|
|
nickname: string;
|
|
};
|
|
}
|
|
|
|
export default function MeContent({ session }: MeContentProps) {
|
|
// 订阅 store 原始引用(稳定,仅在 set() 时变更),组件内 useMemo 派生 supports,
|
|
// 避免 Zustand v5 + useSyncExternalStore 对"selector 返回新引用"报 infinite-loop 错。
|
|
const votedArtists = useVoteStore((s) => s.votedArtists);
|
|
const storeArtists = useVoteStore((s) => s.artists);
|
|
const remaining = useVoteStore(selectRemaining);
|
|
const votedCount = votedArtists.length;
|
|
|
|
const supports = useMemo<MySupport[]>(() => {
|
|
const list: MySupport[] = [];
|
|
for (const id of votedArtists) {
|
|
const artist = storeArtists.find((a) => a.id === id);
|
|
if (artist) list.push({ artist });
|
|
}
|
|
return list;
|
|
}, [votedArtists, storeArtists]);
|
|
|
|
const handleLogout = () => {
|
|
toast("正在退出登录…");
|
|
signOut({ callbackUrl: "/" });
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-[1500px] mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-10 space-y-8">
|
|
<UserHeader
|
|
nickname={session.nickname}
|
|
userId={session.id}
|
|
onLogout={handleLogout}
|
|
/>
|
|
|
|
<QuotaCard remaining={remaining} totalQuota={TOTAL_VOTE_QUOTA} />
|
|
|
|
<StatsGrid voted={votedCount} remaining={remaining} />
|
|
|
|
<section>
|
|
<SectionTitle label="我的应援" />
|
|
<MyFanSupport supports={supports} />
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionTitle({ label }: { label: string }) {
|
|
return (
|
|
<div className="mb-4 flex items-center gap-2.5">
|
|
<span
|
|
aria-hidden
|
|
className="w-1 h-4 rounded-full bg-purple-400 shadow-[0_0_8px_rgba(167,139,250,0.7)]"
|
|
/>
|
|
<h2 className="text-sm font-semibold text-white tracking-wider">
|
|
{label}
|
|
</h2>
|
|
</div>
|
|
);
|
|
}
|