# 投票系统重构 · 完整方案

## 核心规则变更

| 维度 | 旧 | 新 |
|---|---|---|
| 单用户额度 | 每日 10/12 票(跨日重置) | **终身 12 票**(永不重置) |
| 单艺人上限 | 不限 | **每位艺人最多 1 票** |
| 单次投票数 | 可选 1/3/5/ALL | **固定 1 票** |
| 时间窗 | 有 startAt/endAt 倒计时 | **无时间限制** |
| 用户最终行为 | 累积投出 N 票 | **必须选出 12 个不同艺人** |
| 可撤销 | — | **不可撤销** |

## 设计决策(已拍板默认值)

- **A. Hero 进度条**: 12 格点亮式(已投亮紫, 未投暗紫边框, 横向排开)
- **B. VoteModal 确认按钮文案**: `投出我的一票`
- **C. 已投艺人卡片角标**: 右上角紫色 ✓ 圆角标(20px), 阴影柔和
- **D. 持久化**: zustand persist + localStorage(仅 `votedArtists` 字段)

## 本 loop 执行范围

**只动前端**(Store + Hooks + 组件 + 新组件)。
**后端 API/Schema 不动** —— 风险大需要单独迁移, 留给后续 commit。
前端 Store 强制阻止重复投票, API 即使旧的也能正常工作。

---

## 阶段 1 · Store 重构(单文件改重)

### `src/lib/store.ts`

- 删 `DAILY_VOTE_QUOTA = 10` 常量
- 新增 `export const TOTAL_VOTE_QUOTA = 12` 常量
- 删 `usedToday`, `quotaDate` 字段
- `myVotesByArtist: Record<string, number>` → `votedArtists: string[]`
- `MySupport` 接口删 `votedCount` 字段, 只留 `artist`
- `vote(id, count)` → `vote(id): { ok: boolean; reason?: "already" | "exhausted" }`
- `selectRemaining` = `TOTAL_VOTE_QUOTA - votedArtists.length`
- 新增 `selectHasVoted(id)` 高阶函数, 返回 `(s: VoteStore) => boolean`
- 新增 `selectIsExhausted(s) => votedArtists.length >= 12`
- 删 `todayKey()` 函数
- 重写 `selectMySupports` 基于 `votedArtists` 派生
- **加 persist 中间件**:
  - `import { persist } from "zustand/middleware"`
  - 仅持久化 `votedArtists`(partialize)
  - storage key: `cyber-star-vote`

---

## 阶段 2 · 交互闭环

### `src/hooks/useVoteAction.ts`

- import 改用 `TOTAL_VOTE_QUOTA` + `selectHasVoted`
- 接口 `confirmVote(artist, count)` → `confirmVote(artist)`(去 count)
- `openVote` 新增检查: `selectHasVoted(artist.id)` → toast `"你已为 ${artist.name} 投过票"` + abort
- `openVote` `remaining <= 0` 时 toast 改 `"你的 12 票已用完, 感谢支持"`
- `confirmVote` 调用 store.vote 不传 count
- API POST body 去掉 count(`{ artistId }`)
- 成功 toast: `"已为 ${artist.name} 投票 · 剩余 X 票"`
- 第 12 票投完瞬间额外 toast: `"完成!你的 12 票已全部投出"`(里程碑)

### `src/components/VoteModal.tsx`

- 删 `VOTE_OPTIONS` 数组 / `defaultOption` / `resolveCount` / `selected` state
- 删 1/3/5/ALL 选择 grid 整块
- props 去掉 `remaining` 依赖(保留显示用), `dailyQuota` 字段名保留语义改为 totalQuota
- 标题保留 "为 X 投票"
- 副标题保留 `No.${no} · Current Rank #${rank}`
- **新增警示文案**: `"投出后不可撤销, 每位艺人仅能投 1 票"`
- 显示 "剩余 X / 12 票"
- 确认按钮: **`投出我的一票`** (loading 时显示 spinner)
- 已投态(打开时若 hasVoted=true): 标题改 "你已为 TA 投过票了" + 关闭按钮唯一
- 用完态(remaining=0): 标题改 "12 票已用完 · 感谢支持"

---

## 阶段 3 · 已投态 UI(可 sub-agent 并行 3-4 个文件)

### `src/components/cards/ArtistCard.tsx`

- import `selectHasVoted`
- `const hasVoted = useVoteStore(selectHasVoted(artist.id))`
- 投票按钮已投态: `✓ 已投票`, 灰禁用 (`bg-elevated text-white/45 border border-white/10`)
- 卡片右上角加紫色 ✓ 角标(20×20px, `bg-purple-500 rounded-full shadow-purple-glow`), `absolute top-2 right-2 z-10`

### `src/components/ranking/RankingRow.tsx`

- import `selectHasVoted`
- 同样的 hasVoted 判断
- 按钮已投态: `✓ 已投`, 灰禁用
- **顺便修复**: 移除非 Top12 的 `opacity-[0.78]` 暗化(用户已要求过)

### `src/components/artist/ArtistDetailContent.tsx`

- HeroPanel 内的投票按钮: hasVoted → 文案 `✓ 已投票`, `disabled` prop, 紫色淡化
- 头像下方加 ✓ 角标(同 ArtistCard 风格)

### `src/components/FloatingVoteButton.tsx`

- props 新增 `disabled?: boolean`
- disabled=true: Heart 改 Check, 文案改 `已投`, 样式灰
- 调用方 ArtistDetailContent: 透传 `disabled={hasVoted}`

---

## 阶段 4 · 新建 HeroVoteProgress 替换倒计时

### 新建 `src/components/HeroVoteProgress.tsx`

视觉: 12 个小圆点 / 方块横排, 已投亮紫(`bg-purple-500 shadow-purple-glow`), 未投暗紫(`bg-purple-500/15 border border-purple-500/40`)。

```
你的投票  ·  5 / 12
● ● ● ● ● ○ ○ ○ ○ ○ ○ ○
```

状态机:
- 未登录: `登录后开始投票` + 点击触发 LoginModal(用 useLoginModalStore)
- 已投 0/12: 显示标题 "你的投票 · 0 / 12" + 12 格全暗
- 投票中(1-11): 标题 + 进度格子 + 副文案 "还可投 X 位艺人"
- 已投满 12/12: 标题 + 12 格全亮 + ✓ 图标 + 文案 "12 票全部投出 · 感谢支持"

样式约束:
- 紧凑模式, 高度 ≤ 80px(替换原 Countdown 位置)
- 半透明背景 + backdrop-blur(与原 Countdown 视觉一致)
- 文字白色, 进度紫色

### `src/components/HeroBanner.tsx`

- 删 `endTime` prop 和接口字段
- 删 `import Countdown`
- 删右上角倒计时整块
- 替换为 `<HeroVoteProgress />` (位置不变, 右上角)
- 不要删 Countdown.tsx 文件(可能其他场景用)

### `src/app/page.tsx`

- 删 `import { getActivityEndTime }` 和 `const endTime` 行
- `<HeroBanner>` 调用去掉 `endTime` 属性

---

## 阶段 5 · /me 页 + Badge

### `src/components/auth/RemainingVotesBadge.tsx`

- 文案 `今日剩余 X / 10` → `剩余 X / 12`
- 注释 "今日剩余票数" → "剩余票数"
- 未登录显示 `0 / 12`

### `src/components/me/QuotaCard.tsx`

- 标题 `今日剩余票数` → `剩余票数`
- 删 `明日 00:00 重置为 X 票` 整行
- 副文案改 `共 12 票 · 用满完成投票`(已投完则 `12 票全部投出 · 感谢支持`)
- props `dailyQuota` 保留(语义改为 totalQuota, 不重命名避免大面积改动)

### `src/components/me/StatsGrid.tsx`

- 新规则下 "累计投票" 和 "应援艺人" 永远相等 → 删一个
- 改为单 stat 组合: `已投 X / 12 票` + `剩余 12-X 票`(两格)
- 图标 Sparkles → Check(已投), Star → Heart(剩余)

### `src/components/me/MyFanSupport.tsx`

- 移除 `votedCount` 引用
- "已投 X 票" → 紫色 ✓ 徽章 + 文案 "已投票"
- 空态文案: `还没有应援的艺人` → `还没有投过票 · 去为你喜欢的艺人投出第一票`

### `src/app/me/MeContent.tsx`

- `myVotesByArtist` 引用全改 `votedArtists`
- supports useMemo 派生逻辑改为遍历 `votedArtists`
- `myTotalVotes` 改 `votedArtists.length` 或直接删, 用 `.length`
- 传 StatsGrid 的 props 改: `voted={N}` `remaining={12-N}`

---

## 阶段 6 · 验证

### 浏览器手动验证

1. 首页 Hero 区域显示 HeroVoteProgress, 12 格状态正确
2. 点首页某艺人 "投票" → 弹窗显示新文案, 点 "投出我的一票" → toast 成功 + Hero 进度 +1
3. 再点同一艺人 → toast "你已为 TA 投过票", 弹窗不打开
4. 进入 /me 页 → QuotaCard 不显示 "明日重置", StatsGrid 显示新统计, MyFanSupport 显示 ✓ 徽章
5. 投满 12 票 → 触发里程碑 toast, 所有未投艺人按钮变灰禁用
6. 浏览器硬刷新 → votedArtists 从 localStorage 恢复, 已投态保持
7. 排行榜页 → 已投艺人按钮变灰, 未投可点
8. 详情页 HeroPanel 按钮 + FloatingVoteButton 都正确响应 hasVoted

### 编译验证

- dev server 已在 3000 跑着, 改一个文件就 hot-reload 一次, 不要重启
- 检查 dev server log 没有红色 ERROR
- 没有任何 TypeScript 错误(`grep -E "Type error|TS\d+" dev-server.log`)

---

## 阶段 7 · 收尾报告

完成后写报告 `docs/todo/voting-refactor-完成报告.md`, 列:

1. 改了哪些文件(完整列表)
2. 新建了哪些文件
3. 哪些功能验证通过, 哪些边界没测
4. 后续 backend 改动清单(schema unique 约束 / API 检查重投 / API 检查总额度)
5. 已知风险或 TODO

---

## 硬禁区(loop 必须遵守)

- 不要 git push 任何分支
- 不要动 `src/app/api/*` 任何 API route
- 不要动 `prisma/schema.prisma`
- 不要重启 dev server(3000 已经在跑, hot-reload 就行)
- 不要动 TOS 上传 / 视频文件 / 任何资源文件
- 不要碰 `package.json` / `next.config.js`
