diff --git a/CHANGELOG.md b/CHANGELOG.md index 32eabf7..ce9d1e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,38 @@ CYBER STAR(虚拟明星 Top 12 出道选拔)更新日志。新条目写在最上 --- +## v0.3.4 · 2026-05-18 · 跨设备同步 + Logo v3 + 导航合并 + 窄屏适配 + +**Commit 信息** +- 完整 diff: [v0.3.3...v0.3.4](https://gitea.airlabs.art/zyc/UI-UX/compare/v0.3.3...v0.3.4) + +**改了什么(用户视角)** +- 跨设备投票状态自动对齐:A 设备投了 5 票,B 设备登录后立刻看到 5 票已投(原来只看 localStorage) +- 登录页 / 登录弹窗 / Footer 替换为新版金属质感 logo(`logo-v3.png`),Footer 顺手去掉 logo 行 +- 导航栏窄屏不再"双行":"首页 / 排行榜 / 我的"合并到第一行,与右侧搜索/badge/auth 同行 +- 窄屏(< 768px)hero 右上角应援进度只显示文字,隐藏 12 格点 —— 让左侧 "TOP 12 · CYBER STAR" eyebrow 有空间不挤撞 + +**技术点** +- `src/hooks/useSyncMe.ts`(新): 监听 session 变化 → 拉 `/api/me` → `hydrateFromServer(votedArtists)` 覆盖本地 store;登出清本地避免上一个用户残留 +- `src/components/Providers.tsx`: `` 在 SessionProvider 内部启动 useSyncMe +- `src/components/Navigation.tsx`: 删除 mobile 第二行 NavLinks,nav `gap-4 sm:gap-8` 响应式 +- `src/components/NavLinks.tsx`: 删除 mobile/desktop 双分支,统一用 `gap-5 sm:gap-8 text-[13px] sm:text-sm` +- `src/components/HeroVoteProgress.tsx`: 12 格点容器加 `hidden md:inline-flex`,< 768px 隐藏 +- `public/logo-v3.png`(新): 金属质感 logo,替换原 `` 组件 + +**实测三种屏宽**(脚本 `scripts/screenshot-narrow.mjs`) + +| 屏宽 | nav | hero eyebrow | hero progress 宽 | +|------|-----|--------------|------------------| +| 1500px | 单行 ✓ | 不撞 ✓ | 252(完整 12 点)| +| 740px | 单行 ✓ | 不撞 ✓ | 135(隐藏点)| +| 360px | 单行 ✓(NavLinks 140px 塞下)| eyebrow 320px 太长仍占满 ⚠️ | 135 | + +**风险 / 已知问题** +- 360px 极窄屏 hero eyebrow 自身已占 320px,即使 progress 缩到 135 仍会横向重叠。下次单独修(eyebrow 极窄屏可简化为 "TOP 12" 或 hidden sm:block) + +--- + ## v0.3.3 · 2026-05-15 · 修复:投票后票数 +1 和 Top12 排位要等 30s **Commit 信息** diff --git a/package.json b/package.json index 3a445cf..4bc6e6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyber-star", - "version": "0.3.3", + "version": "0.3.4", "private": true, "scripts": { "dev": "next dev", diff --git a/public/logo-v3.png b/public/logo-v3.png new file mode 100644 index 0000000..2cd745f Binary files /dev/null and b/public/logo-v3.png differ diff --git a/scripts/screenshot-narrow.mjs b/scripts/screenshot-narrow.mjs new file mode 100644 index 0000000..bc82756 --- /dev/null +++ b/scripts/screenshot-narrow.mjs @@ -0,0 +1,86 @@ +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-narrow-shot"; +const OUT = process.env.SHOT_OUT || "d:/ClaudeProjects/虚拟明星/UI-UX/docs/screenshots/nav-overlap-narrow.png"; +const WIDTH = Number(process.env.SHOT_WIDTH || 740); +const HEIGHT = Number(process.env.SHOT_HEIGHT || 1000); + +const proc = spawn(CHROME, [ + `--headless=new`, `--disable-gpu`, `--remote-debugging-port=${PORT}`, + `--user-data-dir=${PROFILE}`, `--hide-scrollbars`, `--no-first-run`, `about:blank`, +], { stdio: "ignore", detached: true }); +proc.unref(); +for (let i = 0; i < 30; i++) { + try { if ((await fetch(`http://127.0.0.1:${PORT}/json/version`)).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: WIDTH, height: HEIGHT, deviceScaleFactor: 1, mobile: false, +}); +await cmd("Page.navigate", { url: "http://localhost:3000" }); +await wait(3500); +await cmd("Runtime.evaluate", { + expression: `document.querySelectorAll('video').forEach(v => { try { v.pause(); } catch{} });`, +}); +await wait(500); +const r = await cmd("Page.captureScreenshot", { format: "png" }); +await writeFile(OUT, Buffer.from(r.data, "base64")); + +// 同时取关键元素的位置信息 +const layout = await cmd("Runtime.evaluate", { + returnByValue: true, + expression: `(() => { + const get = sel => { + const el = document.querySelector(sel); + if (!el) return null; + const r = el.getBoundingClientRect(); + return { top: Math.round(r.top), left: Math.round(r.left), width: Math.round(r.width), height: Math.round(r.height), text: el.textContent?.trim().slice(0, 30) ?? '' }; + }; + return { + headerNav: get('header > nav'), + mobileNavLinks: get('header ul.md\\\\:hidden') || get('header [class*="md:hidden"] ul') || (() => { + // mobile NavLinks 在 header 下方第二行,找 header 内最后一个 ul + const uls = document.querySelectorAll('header ul'); + const last = uls[uls.length - 1]; + if (!last) return null; + const r = last.getBoundingClientRect(); + return { top: Math.round(r.top), left: Math.round(r.left), width: Math.round(r.width), height: Math.round(r.height), text: last.textContent?.trim().slice(0, 30) ?? '' }; + })(), + heroEyebrow: (() => { + const els = Array.from(document.querySelectorAll('p')); + const m = els.find(p => /Top 12.*Cyber Star/i.test(p.textContent || '')); + if (!m) return null; + const r = m.getBoundingClientRect(); + return { top: Math.round(r.top), left: Math.round(r.left), width: Math.round(r.width), height: Math.round(r.height), text: m.textContent?.trim().slice(0, 40) ?? '' }; + })(), + heroProgress: get('[data-hero-vote-progress]'), + }; + })()`, +}); +console.log("\n=== 关键元素位置(viewport 坐标)==="); +for (const [k, v] of Object.entries(layout.result.value)) { + if (v) console.log(` ${k.padEnd(16)} top=${String(v.top).padStart(3)}px left=${String(v.left).padStart(3)}px w=${v.width} h=${v.height} "${v.text}"`); + else console.log(` ${k.padEnd(16)} (未找到)`); +} +console.log(`\n✓ ${OUT}`); +ws.close(); +try { process.kill(proc.pid); } catch {} +spawn("taskkill", ["/F", "/PID", String(proc.pid), "/T"], { stdio: "ignore" }); diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx index 20c6360..1a69a99 100644 --- a/src/app/login/LoginForm.tsx +++ b/src/app/login/LoginForm.tsx @@ -5,7 +5,6 @@ import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { signIn } from "next-auth/react"; import { Phone, KeyRound, Loader2 } from "lucide-react"; -import Logo from "@/components/Logo"; import Button from "@/components/ui/Button"; import { cn } from "@/lib/cn"; @@ -92,8 +91,15 @@ export default function LoginForm() {
{/* Logo */} -
- +
+ CYBER STAR

Sign in to Vote

diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 620b481..3abc36d 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,11 +1,8 @@ -import Logo from "./Logo"; - export default function Footer() { const year = new Date().getFullYear(); return (