feat(ui): polish hero/logo/cards + bump TOS version + drop missing-video flags
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m21s

- Hero eyebrow: "Top 12 · Virtual Idol Debut Project"
  → "Top 12 · Cyber Star Debut Survival"
- Hero video: attempt unmuted autoplay first, fall back to muted on
  browser autoplay-policy block (sound button reflects actual state).
- Logo: replace with cropped v2 art, drop purple drop-shadow glow.
- ArtistCard: drop non-top12 opacity dim AND the top dark gradient
  overlay — new high-quality portraits look better fully exposed.
- mock-data: 003/010/017/027/033 solo videos are present in v2,
  cleared MISSING_VIDEO set so the video section renders for them.
- tos: bump TOS_VERSION to 3 — videos/portraits overwritten on TOS,
  this cache-busts older URLs in browsers and CDNs.

TOS uploads (handled separately): hero-pv.mp4, 5 solo videos
(003/010/017/027/033), 7 cover images, 6/036 atmosphere images.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iye 2026-05-15 17:02:29 +08:00
parent 49be38ff77
commit 74a7b0ea16
6 changed files with 24 additions and 16 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -28,14 +28,25 @@ export default function HeroBanner({
className,
}: HeroBannerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const [isMuted, setIsMuted] = useState(true);
// 默认目标:带音播放。但浏览器对"带声音自动播放"有严格限制 ——
// 只有当用户与本站已有交互历史(autoplay policy "high engagement")时才允许。
// 真实初始状态由下面的 effect 试播后写回:成功 → false,被拦截 → true。
const [isMuted, setIsMuted] = useState(false);
useEffect(() => {
const v = videoRef.current;
if (!v || !videoSrc) return;
v.muted = isMuted;
v.play().catch(() => {});
// 仅在 videoSrc 变化时执行 · 不依赖 isMutedmute 切换由按钮处理)
// 先尝试带音播放,失败立刻 fallback 静音播放(并把声音按钮置为静音态)。
// 几乎所有首次访问场景都会走到 fallback,用户点声音按钮再解除静音。
v.muted = false;
v.play()
.then(() => setIsMuted(false))
.catch(() => {
v.muted = true;
setIsMuted(true);
v.play().catch(() => {});
});
// 仅在 videoSrc 变化时执行 · 不依赖 isMuted(mute 切换由按钮处理)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [videoSrc]);
@ -93,7 +104,7 @@ export default function HeroBanner({
{/* Eyebrow 左上 · 紧贴导航下方 */}
<div className="absolute top-[6.5rem] sm:top-[7.5rem] left-4 sm:left-6 lg:left-8 z-10">
<p className="font-label text-[10px] sm:text-xs tracking-[0.4em] uppercase text-purple-200/90">
Top 12 · Virtual Idol Debut Project
Top 12 · Cyber Star Debut Survival
</p>
</div>

View File

@ -25,9 +25,10 @@ export default function Logo({
// 用原生 <img> 绕开 Next/Image 的格式转换 —— 某些环境下 sharp 把透明 PNG
// 转 webp/avif 时会铺白底,导致 logo 在深色 nav 上出现白色矩形。
// ?v=2 缓存破坏:logo 改版时 +1,浏览器立刻拉新版而不读老缓存。
const inner = (
<img
src="/logo.png"
src="/logo.png?v=3"
alt="CYBER STAR"
decoding="async"
draggable={false}
@ -35,8 +36,6 @@ export default function Logo({
height: `${h}px`,
width: "auto",
background: "transparent",
// 保留紫色辉光,但 drop-shadow 不会引入白底
filter: "drop-shadow(0 0 14px rgba(139,92,246,0.4))",
}}
className={`block select-none ${className}`}
/>

View File

@ -40,8 +40,8 @@ export default function ArtistCard({
className="block"
aria-label={`查看 ${artist.name} 详情`}
>
{/* 立绘区13+ 卡片轻度暗化) */}
<div className={cn("relative aspect-[4/5]", !inTop12 && "opacity-[0.78]")}>
{/* 立绘区 · Top12 区分仅靠紫色边框 + 辉光,不再降低非 Top12 卡片亮度 */}
<div className="relative aspect-[4/5]">
<ArtistPortrait
artist={artist}
rounded="rounded-none"
@ -59,9 +59,6 @@ export default function ArtistCard({
>
{artist.rank}
</div>
{/* 顶部轻微渐变蒙层 */}
<div className="absolute inset-x-0 top-0 h-12 bg-gradient-to-b from-black/40 to-transparent pointer-events-none" />
</div>
{/* 信息区(黑色背景明显分隔) */}

View File

@ -11,8 +11,9 @@ import { tosUrl } from "./tos";
* / store
*/
/** 没有 solo.mp4 的艺人编号docx 标注"缺视频" */
const MISSING_VIDEO: ReadonlySet<string> = new Set(["003", "010", "017", "027"]);
/** solo.mp4 (docx "")
003/010/017/027 v2 ,033 , */
const MISSING_VIDEO: ReadonlySet<string> = new Set<string>();
/**
* 自定义封面:这些艺人的卡片/ cover ,

View File

@ -14,7 +14,7 @@
* , TTL invalidate
*/
const TOS_BASE = (process.env.NEXT_PUBLIC_TOS_DOMAIN ?? "").replace(/\/+$/, "");
const TOS_VERSION = "2";
const TOS_VERSION = "3";
export function tosUrl(path: string): string {
const clean = path.replace(/^\/+/, "");