元旦节皮肤

This commit is contained in:
eafonyang
2026-10-09 13:56:36 +08:00
parent 7e6995deaf
commit 4ce89e8cc2
16 changed files with 449 additions and 7 deletions
+9 -2
View File
@@ -39,7 +39,7 @@ interface NavItem {
const FORUM_URL = "https://forum.zidoo.tv/index.php";
const festivalIcon = (festival: FestivalId, name: string) =>
`${import.meta.env.BASE_URL}images/festival/${festival}/${name}.png`;
`${import.meta.env.BASE_URL}images/festival/${festival}/${name}.${festival === "newyear" ? "svg" : "png"}`;
const NAV_ITEMS: NavItem[] = [
{
@@ -49,6 +49,7 @@ const NAV_ITEMS: NavItem[] = [
festivalIcons: {
christmas: festivalIcon("christmas", "bell"),
halloween: festivalIcon("halloween", "ghost"),
newyear: festivalIcon("newyear", "confetti"),
},
},
{
@@ -58,6 +59,7 @@ const NAV_ITEMS: NavItem[] = [
festivalIcons: {
christmas: festivalIcon("christmas", "reindeer"),
halloween: festivalIcon("halloween", "skeleton"),
newyear: festivalIcon("newyear", "clock"),
},
},
{
@@ -67,6 +69,7 @@ const NAV_ITEMS: NavItem[] = [
festivalIcons: {
christmas: festivalIcon("christmas", "santa"),
halloween: festivalIcon("halloween", "pumpkin"),
newyear: festivalIcon("newyear", "calendar"),
},
},
{
@@ -76,6 +79,7 @@ const NAV_ITEMS: NavItem[] = [
festivalIcons: {
christmas: festivalIcon("christmas", "tree"),
halloween: festivalIcon("halloween", "witch"),
newyear: festivalIcon("newyear", "fireworks"),
},
},
{
@@ -85,6 +89,7 @@ const NAV_ITEMS: NavItem[] = [
festivalIcons: {
christmas: festivalIcon("christmas", "gift"),
halloween: festivalIcon("halloween", "vampire"),
newyear: festivalIcon("newyear", "party-hat"),
},
},
];
@@ -102,7 +107,9 @@ export default function BottomNav() {
}, [deviceState?.language]);
return (
<nav className={bottomNavOuterClass}>
<nav
className={bottomNavOuterClass}
>
<div
className={bottomNavInnerClass}
style={{
@@ -1,6 +1,7 @@
import { useLayoutEffect, useRef, useState } from "react";
import { FESTIVAL_GREETINGS, type FestivalId } from "@/lib/festival";
import { FESTIVAL_GREETINGS, getNewYearDisplayYear, type FestivalId } from "@/lib/festival";
import "./festival.css";
import "./newyear.css";
/**
* VU 区的静态插画横幅:两端角色保持比例,中央背景平铺,文字独立缩放。
@@ -15,6 +16,8 @@ export default function FestivalGreeting({
const textBoxRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
const [newYear] = useState(() => getNewYearDisplayYear());
const isNewYear = festival === "newyear";
const artworkRoot = `${import.meta.env.BASE_URL}images/festival/${festival}/`;
useLayoutEffect(() => {
@@ -60,7 +63,7 @@ export default function FestivalGreeting({
ref={boxRef}
className={`festival-greeting festival-greeting--${festival}`}
role="img"
aria-label={FESTIVAL_GREETINGS[festival]}
aria-label={isNewYear ? `${FESTIVAL_GREETINGS[festival]} ${newYear}` : FESTIVAL_GREETINGS[festival]}
>
<div
className="festival-greeting-backdrop"
@@ -78,6 +81,15 @@ export default function FestivalGreeting({
/>
))}
<div className="festival-greeting-shade" aria-hidden="true" />
{isNewYear && (
<img
src={`${artworkRoot}calendar.svg`}
className="newyear-greeting-calendar"
alt=""
aria-hidden="true"
draggable={false}
/>
)}
<div
ref={textBoxRef}
className="festival-greeting-title"
@@ -88,7 +100,20 @@ export default function FestivalGreeting({
className={`${festival}-greeting-text festival-greeting-inner`}
style={{ transform: `scale(${scale})` }}
>
{FESTIVAL_GREETINGS[festival].split(" ").map(word => (
{isNewYear ? (
<>
<div className="newyear-greeting-heading">
<div className="newyear-greeting-words">
<span>Happy</span>
<span>New Year</span>
</div>
<strong className="newyear-greeting-year">{newYear}</strong>
</div>
<span className="newyear-greeting-tagline">
NEW YEAR · NEW CHAPTER · MORE POSSIBILITIES
</span>
</>
) : FESTIVAL_GREETINGS[festival].split(" ").map(word => (
<span key={word} className="festival-greeting-line">
{word}
</span>
@@ -0,0 +1,58 @@
import { memo } from "react";
import { cn } from "@/lib/utils";
import "./newyear.css";
const ROOT = `${import.meta.env.BASE_URL}images/festival/newyear/`;
/** 新年礼帽与仙女棒只替换视觉层,拖拽命中和音量提交仍由滑块管理。 */
export const NewYearVolumeThumb = memo(function NewYearVolumeThumb() {
return (
<img
src={`${ROOT}party-hat.svg`}
alt=""
draggable={false}
className="newyear-volume-thumb"
/>
);
});
/** 两端占位仍为 24px:最小音量是小仙女棒,最大音量是绽放的烟花。 */
export function NewYearVolumeIcon({ dim = false }: { dim?: boolean }) {
return (
<span className="newyear-volume-icon-slot" aria-hidden="true">
<img
src={`${ROOT}${dim ? "sparkler" : "fireworks"}.svg`}
alt=""
draggable={false}
className={cn("newyear-volume-icon", dim && "newyear-volume-icon--dim")}
/>
</span>
);
}
/** 卡片顶部的金色彩带与星屑,不覆盖端口、不拦截指针事件。 */
export const NewYearCardDecoration = memo(function NewYearCardDecoration() {
return (
<div className="newyear-card-decoration" aria-hidden="true">
<svg viewBox="0 0 600 20" preserveAspectRatio="none" focusable="false">
<path
d="M0 2Q150 19 300 5T600 2"
fill="none"
stroke="#e7bd6b"
strokeOpacity="0.35"
strokeWidth="0.7"
vectorEffect="non-scaling-stroke"
/>
</svg>
{[7, 20, 35, 51, 67, 82, 95].map((left, index) => (
<span
key={left}
className="newyear-card-star"
style={{ left: `${left}%`, top: index % 2 ? 7 : 2 }}
>
{index % 2 ? "·" : "✦"}
</span>
))}
</div>
);
});
+152
View File
@@ -0,0 +1,152 @@
/* 元旦视觉层:保留滑轨尺寸、青色音量反馈与所有交互热区。 */
.newyear-volume-thumb {
display: block;
width: 44px;
height: 44px;
max-width: none;
object-fit: contain;
pointer-events: none;
user-select: none;
filter: drop-shadow(0 2px 4px #000) drop-shadow(0 0 3px rgba(242, 196, 98, 0.35));
}
.newyear-volume-icon-slot {
display: block;
position: relative;
width: 24px;
height: 24px;
pointer-events: none;
}
.newyear-volume-icon {
position: absolute;
left: 50%;
top: 50%;
width: 32px;
height: 32px;
max-width: none;
transform: translate(-50%, -50%);
filter: drop-shadow(0 0 3px rgba(242, 196, 98, 0.3));
pointer-events: none;
user-select: none;
}
.newyear-volume-icon--dim {
width: 27px;
height: 27px;
opacity: 0.75;
}
.newyear-card-decoration {
position: absolute;
inset: 0 0 auto;
height: 20px;
z-index: 1;
overflow: hidden;
pointer-events: none;
user-select: none;
}
.newyear-card-decoration * {
pointer-events: none;
}
.newyear-card-decoration > svg {
width: 100%;
height: 20px;
}
.newyear-card-star {
position: absolute;
color: #e7bd6b;
opacity: 0.75;
font-size: 9px;
line-height: 1;
}
.festival-greeting--newyear {
background: #071221;
}
.festival-greeting--newyear .festival-greeting-art {
opacity: 0.75;
}
.festival-greeting--newyear .festival-greeting-shade {
background: linear-gradient(90deg, transparent, rgba(3, 9, 20, 0.3) 40%, rgba(3, 9, 20, 0.3) 60%, transparent);
}
/* 标题整体仍由共享组件测量缩放,窄屏按三行排列,宽屏增加副标题。 */
.festival-greeting.festival-greeting--newyear[data-layout] .newyear-greeting-text {
flex-direction: column;
color: #ffe8ae;
text-shadow: 0 1px 1px #71390d, 0 0 8px rgba(236, 145, 52, 0.6);
}
.newyear-greeting-heading,
.newyear-greeting-words {
display: flex;
flex-direction: column;
align-items: center;
}
.newyear-greeting-words {
font-family: "Snell Roundhand", "Apple Chancery", "URW Chancery L", Georgia, cursive;
font-size: 36px;
font-style: italic;
line-height: 1.05;
}
.newyear-greeting-year {
font-family: Georgia, "Times New Roman", serif;
font-size: 43px;
font-style: italic;
font-weight: 700;
letter-spacing: 0.03em;
line-height: 1;
color: #ffd57a;
}
.newyear-greeting-tagline {
display: none;
font-family: Georgia, "Times New Roman", serif;
font-size: 8px;
line-height: 1.6;
letter-spacing: 2px;
color: #f5da9b;
text-shadow: 0 1px 3px #000;
}
.newyear-greeting-calendar {
display: none;
position: absolute;
width: 48px;
height: 48px;
left: 22px;
bottom: 4px;
transform: rotate(-9deg);
filter: drop-shadow(0 2px 4px #000);
}
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-calendar {
display: block;
}
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-heading,
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-words {
flex-direction: row;
}
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-words > span + span {
margin-left: 0.22em;
}
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-year {
margin-left: 18px;
font-size: 54px;
}
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-tagline {
display: block;
margin-top: 4px;
}
+9 -1
View File
@@ -6,7 +6,7 @@
============================================================ */
import { useMemo } from "react";
export type FestivalId = "christmas" | "halloween";
export type FestivalId = "christmas" | "halloween" | "newyear";
/** Inclusive [month, day] windows. Christmas: Dec 20 – Dec 26; Halloween: Oct 26 – Nov 1. */
const FESTIVAL_WINDOWS: Record<
@@ -15,6 +15,8 @@ const FESTIVAL_WINDOWS: Record<
> = {
christmas: { from: [12, 20], to: [12, 26] },
halloween: { from: [10, 26], to: [11, 1] },
// 元旦接续圣诞节,跨年窗口包含首尾日期。
newyear: { from: [12, 27], to: [1, 3] },
};
function inWindow(
@@ -55,8 +57,14 @@ export function useFestival(): FestivalId | null {
return useMemo(() => getFestival(), []);
}
/** 跨年夜展示即将到来的年份,其余月份预览展示当年。 */
export function getNewYearDisplayYear(now: Date = new Date()): number {
return now.getFullYear() + (now.getMonth() === 11 ? 1 : 0);
}
/** 首页 VU 区的节日问候语,键与 FestivalId 对应。 */
export const FESTIVAL_GREETINGS: Record<FestivalId, string> = {
christmas: "Merry Christmas",
halloween: "Happy Halloween",
newyear: "Happy New Year",
};
+14 -1
View File
@@ -20,6 +20,11 @@ import HalloweenVolumeThumb from "@/components/festival/HalloweenVolumeThumb";
import HalloweenPumpkinIcon from "@/components/festival/HalloweenPumpkinIcon";
import HalloweenCardDecoration from "@/components/festival/HalloweenCardDecoration";
import FestivalGreeting from "@/components/festival/FestivalGreeting";
import {
NewYearCardDecoration,
NewYearVolumeIcon,
NewYearVolumeThumb,
} from "@/components/festival/NewYearDecoration";
import "@/components/festival/christmas.css";
import "@/components/festival/halloween.css";
import "@/components/festival/festival.css";
@@ -200,6 +205,7 @@ export default function Home() {
const festival = useFestival();
const isChristmas = festival === "christmas";
const isHalloween = festival === "halloween";
const isNewYear = festival === "newyear";
const powerActionRef = useRef(false);
const hpEqLastTapRef = useRef<number | null>(null);
const hpEqNavigateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -529,6 +535,7 @@ export default function Home() {
style={{ background: "rgba(18,18,20,0.8)", minHeight: 140 }}>
{isChristmas && <ChristmasCardDecoration />}
{isHalloween && <HalloweenCardDecoration />}
{isNewYear && <NewYearCardDecoration />}
{/* Stylized device illustration */}
<div className="w-full relative" style={{ height: 140 }}>
{/* Device body */}
@@ -676,6 +683,8 @@ export default function Home() {
<ChristmasBellIcon dim />
) : isHalloween ? (
<HalloweenPumpkinIcon dim />
) : isNewYear ? (
<NewYearVolumeIcon dim />
) : (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-6 h-6">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
@@ -718,7 +727,9 @@ export default function Home() {
? () => <ChristmasVolumeThumb />
: isHalloween
? () => <HalloweenVolumeThumb />
: undefined
: isNewYear
? () => <NewYearVolumeThumb />
: undefined
}
/>
</div>
@@ -736,6 +747,8 @@ export default function Home() {
<ChristmasBellIcon />
) : isHalloween ? (
<HalloweenPumpkinIcon />
) : isNewYear ? (
<NewYearVolumeIcon />
) : (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-6 h-6">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>