diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd5c8a..54a6529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,29 @@ CYBER STAR(虚拟明星 Top 12 出道选拔)更新日志。新条目写在最上 --- +## v0.3.2 · 2026-05-15 · 修复:首页 Top12 出道位空着,排行榜却有数据 + +**Commit 信息** +- 完整 diff: [v0.3.1...v0.3.2](https://gitea.airlabs.art/zyc/UI-UX/compare/v0.3.1...v0.3.2) + +**改了什么** +- 首页 Top12 出道位现在能显示真实票数(此前一直显示"Awaiting Votes") + +**根因** +- 首页 Top12Bar 之前只读前端 zustand store 的 `artists`,store 初始来自 mock-data(36 人全部 0 票),只有**当前浏览器用户自己投票时**才会更新 +- 排行榜 `/ranking` 走 `/api/ranking` 直接读 DB,所以有真实票数 +- 未登录访客访问首页 → store 全 0 票 → `top12 = filter(votes > 0)` 空 → 显示"Awaiting Votes" + +**技术修复** +- `src/app/page.tsx` 加 `useRanking({ pollInterval: 30_000 })`,30 秒轮询 `/api/ranking` +- 用 `useMemo` 合并 `storeArtists`(本地乐观投票)+ API 票数(取 max),重新排序赋 rank +- 与 `/ranking` 页面用同样的 merge 策略,保证两处票数视图一致 + +**风险 / 已知问题** +- 首页和排行榜各自跑一个 `useRanking` 实例,会产生两个独立轮询请求。后续可以提到 layout 层共享,但当前 36 行查询很轻,先这样 + +--- + ## v0.3.1 · 2026-05-15 · 13 号(虞浓)氛围图 2 替换 **Commit 信息** diff --git a/package.json b/package.json index 9e0c39a..07a458a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyber-star", - "version": "0.3.1", + "version": "0.3.2", "private": true, "scripts": { "dev": "next dev", diff --git a/scripts/verify-top12.mjs b/scripts/verify-top12.mjs new file mode 100644 index 0000000..5f91fc8 --- /dev/null +++ b/scripts/verify-top12.mjs @@ -0,0 +1,76 @@ +import { spawn } from "node:child_process"; +import { writeFile } from "node:fs/promises"; +import { setTimeout as wait } from "node:timers/promises"; + +const CHROME = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"; +const PORT = 9333; +const PROFILE = "C:\\Users\\10419\\AppData\\Local\\Temp\\cs-top12-verify"; + +const proc = spawn( + CHROME, + [ + `--headless=new`, + `--disable-gpu`, + `--remote-debugging-port=${PORT}`, + `--user-data-dir=${PROFILE}`, + `--window-size=1500,900`, + `--hide-scrollbars`, + `--no-first-run`, + `about:blank`, + ], + { stdio: "ignore", detached: true }, +); +proc.unref(); +for (let i = 0; i < 30; i++) { + try { + const r = await fetch(`http://127.0.0.1:${PORT}/json/version`); + if (r.ok) break; + } catch {} + await wait(300); +} + +const t = await (await fetch(`http://127.0.0.1:${PORT}/json/new?about:blank`, { method: "PUT" })).json(); +const ws = new WebSocket(t.webSocketDebuggerUrl); +await new Promise((r) => ws.addEventListener("open", () => r(), { once: true })); +let id = 0; +const pending = new Map(); +ws.addEventListener("message", (e) => { + const m = JSON.parse(e.data); + if (m.id && pending.has(m.id)) { + const { resolve, reject } = pending.get(m.id); + pending.delete(m.id); + if (m.error) reject(new Error(m.error.message)); + else resolve(m.result); + } +}); +const cmd = (method, params = {}) => + new Promise((resolve, reject) => { + pending.set(++id, { resolve, reject }); + ws.send(JSON.stringify({ id, method, params })); + }); + +await cmd("Emulation.setDeviceMetricsOverride", { + width: 1500, height: 900, deviceScaleFactor: 1, mobile: false, +}); +await cmd("Page.navigate", { url: "http://localhost:3000" }); +await wait(3000); +await cmd("Runtime.evaluate", { + expression: `document.querySelectorAll('video').forEach(v => { try { v.pause(); } catch{} });`, +}); +// 滚到 Top12 区 +await cmd("Runtime.evaluate", { + expression: `window.scrollTo(0, window.innerHeight);`, +}); +await wait(1500); +// 多等一下 useRanking 拉到数据 +await wait(2500); + +const r = await cmd("Page.captureScreenshot", { format: "png" }); +await writeFile( + "d:/ClaudeProjects/虚拟明星/UI-UX/docs/screenshots/top12-fix-verify.png", + Buffer.from(r.data, "base64"), +); +console.log("✓ saved"); +ws.close(); +try { process.kill(proc.pid); } catch {} +spawn("taskkill", ["/F", "/PID", String(proc.pid), "/T"], { stdio: "ignore" }); diff --git a/src/app/page.tsx b/src/app/page.tsx index a689a50..cbf9a14 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,16 +10,21 @@ import VoteModal from "@/components/VoteModal"; import { sortArtists, type SortKey } from "@/lib/mock-data"; import { useVoteStore } from "@/lib/store"; import { useVoteAction } from "@/hooks/useVoteAction"; +import { useRanking } from "@/hooks/useRanking"; import { useScrollRestore } from "@/hooks/useScrollRestore"; import { useUIStore } from "@/lib/ui-store"; import { cn } from "@/lib/cn"; import { tosUrl } from "@/lib/tos"; +import type { Artist } from "@/types/artist"; export default function Home() { - const artists = useVoteStore((s) => s.artists); + const storeArtists = useVoteStore((s) => s.artists); const { target, remaining, totalQuota, openVote, closeVote, confirmVote } = useVoteAction(); + // 30s 轮询 /api/ranking 拿服务端真实票数 + const live = useRanking({ pollInterval: 30_000 }); + const [tagFilter, setTagFilter] = useState("all"); const [sortKey, setSortKey] = useState("votes"); const [filterStuck, setFilterStuck] = useState(false); @@ -29,6 +34,21 @@ export default function Home() { // 首页滚动位置 per-tab 记忆:从艺人详情点 ← 返回时恢复到上次浏览位置 useScrollRestore("home"); + // 数据同步:本地乐观投票 + 服务端票数取 max,避免本地 store 只看到自己投的票。 + // 与 /ranking 页面同一策略,首页 Top12 + 候选区都基于这份合并数据。 + const artists = useMemo(() => { + const apiVotes = new Map(); + if (live.data?.list) { + for (const row of live.data.list) apiVotes.set(row.id, row.voteCount); + } + const merged = storeArtists.map((a) => { + const apiV = apiVotes.get(a.id) ?? 0; + return apiV > a.votes ? { ...a, votes: apiV } : a; + }); + const ranked = sortArtists(merged, "votes"); + return ranked.map((a, i) => ({ ...a, rank: i + 1 })); + }, [storeArtists, live.data]); + const visibleArtists = useMemo(() => { let list = [...artists]; if (tagFilter !== "all") {