Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51009616a1 | |||
| e05f63b94f |
23
CHANGELOG.md
23
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 替换
|
## v0.3.1 · 2026-05-15 · 13 号(虞浓)氛围图 2 替换
|
||||||
|
|
||||||
**Commit 信息**
|
**Commit 信息**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cyber-star",
|
"name": "cyber-star",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
59
scripts/reset-vote-data.mjs
Normal file
59
scripts/reset-vote-data.mjs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// 清空投票相关测试数据,准备重新测试。
|
||||||
|
//
|
||||||
|
// 清:votes / fan_supports / daily_quota / sign_ins / risk_logs / ranking_snapshots
|
||||||
|
// reset:artists.vote_count = 0, artists.current_rank = null
|
||||||
|
// 保留:users / artists 配置 / activity_config / invitations
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
const prisma = new PrismaClient({ log: ["error"] });
|
||||||
|
|
||||||
|
console.log("=== 清空投票测试数据 ===\n");
|
||||||
|
|
||||||
|
// 跑前 snapshot
|
||||||
|
const before = {
|
||||||
|
votes: await prisma.vote.count(),
|
||||||
|
fanSupports: await prisma.fanSupport.count(),
|
||||||
|
dailyQuotas: await prisma.dailyQuota.count(),
|
||||||
|
signIns: await prisma.signIn.count(),
|
||||||
|
riskLogs: await prisma.riskLog.count(),
|
||||||
|
snapshots: await prisma.rankingSnapshot.count(),
|
||||||
|
artistsWithVotes: await prisma.artist.count({ where: { voteCount: { gt: 0 } } }),
|
||||||
|
};
|
||||||
|
console.log("清前:", before);
|
||||||
|
|
||||||
|
await prisma.$transaction(
|
||||||
|
async (tx) => {
|
||||||
|
await tx.vote.deleteMany({});
|
||||||
|
await tx.fanSupport.deleteMany({});
|
||||||
|
await tx.dailyQuota.deleteMany({});
|
||||||
|
await tx.signIn.deleteMany({});
|
||||||
|
await tx.riskLog.deleteMany({});
|
||||||
|
await tx.rankingSnapshot.deleteMany({});
|
||||||
|
// reset artists 缓存
|
||||||
|
await tx.artist.updateMany({
|
||||||
|
data: { voteCount: 0, currentRank: null },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ timeout: 30000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const after = {
|
||||||
|
votes: await prisma.vote.count(),
|
||||||
|
fanSupports: await prisma.fanSupport.count(),
|
||||||
|
dailyQuotas: await prisma.dailyQuota.count(),
|
||||||
|
signIns: await prisma.signIn.count(),
|
||||||
|
riskLogs: await prisma.riskLog.count(),
|
||||||
|
snapshots: await prisma.rankingSnapshot.count(),
|
||||||
|
artistsWithVotes: await prisma.artist.count({ where: { voteCount: { gt: 0 } } }),
|
||||||
|
};
|
||||||
|
console.log("\n清后:", after);
|
||||||
|
|
||||||
|
// 同时确认保留了什么
|
||||||
|
const preserved = {
|
||||||
|
users: await prisma.user.count(),
|
||||||
|
artists: await prisma.artist.count(),
|
||||||
|
activityConfig: await prisma.activityConfig.count(),
|
||||||
|
};
|
||||||
|
console.log("保留:", preserved);
|
||||||
|
|
||||||
|
await prisma.$disconnect();
|
||||||
|
console.log("\n✓ 完成");
|
||||||
76
scripts/verify-top12.mjs
Normal file
76
scripts/verify-top12.mjs
Normal file
@ -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" });
|
||||||
@ -10,16 +10,21 @@ import VoteModal from "@/components/VoteModal";
|
|||||||
import { sortArtists, type SortKey } from "@/lib/mock-data";
|
import { sortArtists, type SortKey } from "@/lib/mock-data";
|
||||||
import { useVoteStore } from "@/lib/store";
|
import { useVoteStore } from "@/lib/store";
|
||||||
import { useVoteAction } from "@/hooks/useVoteAction";
|
import { useVoteAction } from "@/hooks/useVoteAction";
|
||||||
|
import { useRanking } from "@/hooks/useRanking";
|
||||||
import { useScrollRestore } from "@/hooks/useScrollRestore";
|
import { useScrollRestore } from "@/hooks/useScrollRestore";
|
||||||
import { useUIStore } from "@/lib/ui-store";
|
import { useUIStore } from "@/lib/ui-store";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { tosUrl } from "@/lib/tos";
|
import { tosUrl } from "@/lib/tos";
|
||||||
|
import type { Artist } from "@/types/artist";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const artists = useVoteStore((s) => s.artists);
|
const storeArtists = useVoteStore((s) => s.artists);
|
||||||
const { target, remaining, totalQuota, openVote, closeVote, confirmVote } =
|
const { target, remaining, totalQuota, openVote, closeVote, confirmVote } =
|
||||||
useVoteAction();
|
useVoteAction();
|
||||||
|
|
||||||
|
// 30s 轮询 /api/ranking 拿服务端真实票数
|
||||||
|
const live = useRanking({ pollInterval: 30_000 });
|
||||||
|
|
||||||
const [tagFilter, setTagFilter] = useState<TagFilter>("all");
|
const [tagFilter, setTagFilter] = useState<TagFilter>("all");
|
||||||
const [sortKey, setSortKey] = useState<SortKey>("votes");
|
const [sortKey, setSortKey] = useState<SortKey>("votes");
|
||||||
const [filterStuck, setFilterStuck] = useState(false);
|
const [filterStuck, setFilterStuck] = useState(false);
|
||||||
@ -29,6 +34,21 @@ export default function Home() {
|
|||||||
// 首页滚动位置 per-tab 记忆:从艺人详情点 ← 返回时恢复到上次浏览位置
|
// 首页滚动位置 per-tab 记忆:从艺人详情点 ← 返回时恢复到上次浏览位置
|
||||||
useScrollRestore("home");
|
useScrollRestore("home");
|
||||||
|
|
||||||
|
// 数据同步:本地乐观投票 + 服务端票数取 max,避免本地 store 只看到自己投的票。
|
||||||
|
// 与 /ranking 页面同一策略,首页 Top12 + 候选区都基于这份合并数据。
|
||||||
|
const artists = useMemo<Artist[]>(() => {
|
||||||
|
const apiVotes = new Map<string, number>();
|
||||||
|
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(() => {
|
const visibleArtists = useMemo(() => {
|
||||||
let list = [...artists];
|
let list = [...artists];
|
||||||
if (tagFilter !== "all") {
|
if (tagFilter !== "all") {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user