diff --git a/app/components/ActivityTicker.tsx b/app/components/ActivityTicker.tsx new file mode 100644 index 00000000..acb26ef2 --- /dev/null +++ b/app/components/ActivityTicker.tsx @@ -0,0 +1,162 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import eventsData from "@/data/event.json"; +import type { ActivityEvent, ActivityEventsConfig } from "@/app/types/event"; +import { cn } from "@/lib/utils"; + +const { + events: rawEvents, + settings: { + maxItems: configuredMaxItems = 3, + rotationIntervalMs: configuredRotationIntervalMs = 8000, + }, +} = eventsData as ActivityEventsConfig; + +// 默认配置,从data/event.json中读取配置 +const MAX_ITEMS = configuredMaxItems; +const ROTATION_INTERVAL_MS = configuredRotationIntervalMs; + +/** ActivityTicker 外部传入的样式配置 */ +type ActivityTickerProps = { + /** 容器额外类名,用于控制宽度与定位 */ + className?: string; +}; + +/** + * 首页活动轮播组件: + * - 读取 event.json 配置的活动数量 + * - 自动轮播封面图,顶部指示器支持手动切换 + * - 底部两个毛玻璃按钮:Discord 永远可见,Playback 仅在 deprecated=true 时显示 + */ +export function ActivityTicker({ className }: ActivityTickerProps) { + // 预处理活动列表,保持初次渲染后的引用稳定 + const events = useMemo(() => { + return rawEvents.slice(0, MAX_ITEMS); + }, []); + + // 当前展示的活动索引 + const [activeIndex, setActiveIndex] = useState(0); + const totalEvents = events.length; + + useEffect(() => { + if (totalEvents <= 1) { + return; + } + + // 定时轮播,间隔 ROTATION_INTERVAL_MS + const timer = window.setInterval(() => { + setActiveIndex((prev) => (prev + 1) % totalEvents); + }, ROTATION_INTERVAL_MS); + + return () => window.clearInterval(timer); + }, [totalEvents, activeIndex]); + + const handlePrev = useCallback(() => { + if (totalEvents <= 1) { + return; + } + setActiveIndex((prev) => (prev - 1 + totalEvents) % totalEvents); + }, [totalEvents]); + + const handleNext = useCallback(() => { + if (totalEvents <= 1) { + return; + } + setActiveIndex((prev) => (prev + 1) % totalEvents); + }, [totalEvents]); + + if (totalEvents === 0) { + return null; + } + + const activeEvent = events[activeIndex]; + const coverSrc = activeEvent.coverUrl; + const showPlayback = activeEvent.deprecated && Boolean(activeEvent.playback); + + return ( + + ); +} diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 399e405c..88588755 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import ZoteroFeedLazy from "@/app/components/ZoteroFeedLazy"; import { Contribute } from "@/app/components/Contribute"; import Image from "next/image"; +import { ActivityTicker } from "@/app/components/ActivityTicker"; export function Hero() { const categories: { title: string; desc: string; href: string }[] = [ @@ -29,7 +30,14 @@ export function Hero() { return (
-
+
+ {/* 首页活动轮播浮窗:桌面端右上角,移动端底部居中 */} +
+ +
+
+ +