video-flow-b/components/ui/VideoCoverflow.tsx
2025-09-26 21:04:10 +08:00

177 lines
7.2 KiB
TypeScript

"use client";
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, EffectCoverflow, EffectCards } from 'swiper/modules';
import type { Swiper as SwiperType } from 'swiper/types';
import { useDeviceType } from '@/hooks/useDeviceType';
import 'swiper/css';
import 'swiper/css/effect-coverflow';
import 'swiper/css/effect-cards';
/** 默认视频列表(来自 home-page2.tsx 中的数组) */
const DEFAULT_VIDEOS: string[] = [
'https://cdn.qikongjian.com/1756474023656_60twk5.mp4',
'https://cdn.qikongjian.com/1756474023644_14n7is.mp4',
'https://cdn.qikongjian.com/1756474023648_kocq6z.mp4',
'https://cdn.qikongjian.com/1756474023657_w10boo.mp4',
'https://cdn.qikongjian.com/1756474023657_nf8799.mp4',
'https://cdn.qikongjian.com/1756474230992_vw0ubf.mp4',
];
export interface VideoCoverflowProps {
title?: string;
subtitle?: string;
/** 视频地址数组 */
videos?: string[];
/** 自动播放间隔(毫秒) */
autoplayDelay?: number;
}
/**
* 使用 Swiper 的 Coverflow 效果展示视频,自动滚动并仅播放当前居中的视频。
* @param {VideoCoverflowProps} props - 组件属性
* @returns {JSX.Element} - 组件节点
*/
const VideoCoverflow: React.FC<VideoCoverflowProps> = ({
title = '',
subtitle = '',
videos = DEFAULT_VIDEOS,
autoplayDelay = 4500,
}) => {
const swiperRef = React.useRef<SwiperType | null>(null);
const videoRefs = React.useRef<Record<number, HTMLVideoElement | null>>({});
const { isMobile } = useDeviceType();
const [activeIndex, setActiveIndex] = React.useState<number>(0);
const playActive = React.useCallback((activeIndex: number) => {
Object.entries(videoRefs.current).forEach(([key, el]) => {
const video = el as HTMLVideoElement | null;
if (!video) return;
const index = Number(key);
if (index === activeIndex) {
// 尝试播放当前居中视频
video.play().catch(() => {});
} else {
// 暂停其他视频,重置到起点以减少解码负担
video.pause();
try {
video.currentTime = 0;
} catch {}
}
});
}, []);
const handleAfterInit = React.useCallback((sw: SwiperType) => {
swiperRef.current = sw;
const idx = sw.realIndex ?? sw.activeIndex ?? 0;
setActiveIndex(idx);
playActive(idx);
}, [playActive]);
const handleSlideChange = React.useCallback((sw: SwiperType) => {
const idx = sw.realIndex ?? sw.activeIndex ?? 0;
setActiveIndex(idx);
playActive(idx);
}, [playActive]);
return (
<div
data-alt="video-coverflow-container"
className="center h-[calc(100svh-4rem)] z-10 flex flex-col items-center justify-center lg:px-20 xl:px-30 2xl:px-40"
>
<h2
className="text-white font-normal text-center
-mt-4
/* 移动端字体 */
text-[2rem] leading-[110%] mb-4
/* 平板字体 */
sm:-mt-6 sm:text-[2.5rem] sm:leading-[110%] sm:mb-6
/* 小屏笔记本字体 */
md:-mt-8 md:text-[3rem] md:leading-[110%] md:mb-8
/* 大屏笔记本字体 */
lg:-mt-10 lg:text-[3.25rem] lg:leading-[110%] lg:mb-10
/* 桌面端字体 */
xl:-mt-12 xl:text-[3.375rem] xl:leading-[110%] xl:mb-12
/* 大屏显示器字体 */
2xl:-mt-16 2xl:text-[3.5rem] 2xl:leading-[110%] 2xl:mb-16"
>
{title}
</h2>
<p className="text-white font-normal text-center mb-8
-mt-2
/* 移动端字体 */
text-[1rem] leading-[140%]
/* 平板字体 */
sm:-mt-3 sm:text-[1.25rem] sm:leading-[140%]
/* 小屏笔记本字体 */
md:-mt-4 md:text-[1.5rem] md:leading-[140%]
/* 大屏笔记本字体 */
lg:-mt-5 lg:text-[1.6rem] lg:leading-[140%]
/* 桌面端字体 */
xl:-mt-6 xl:text-[1.7rem] xl:leading-[140%]
/* 大屏显示器字体 */
2xl:-mt-8 2xl:text-[1.8rem] 2xl:leading-[140%]"
>
{subtitle}
</p>
<div data-alt="video-coverflow" className="w-screen sm:w-full mx-auto overflow-hidden">
<Swiper
modules={isMobile ? [Autoplay, EffectCards] : [Autoplay, EffectCoverflow]}
effect={isMobile ? 'cards' : 'coverflow'}
key={isMobile ? 'cards' : 'coverflow'}
centeredSlides
slidesPerView={isMobile ? 1 : 2}
loop
autoplay={{ delay: autoplayDelay, disableOnInteraction: false }}
speed={1000}
{...(!isMobile ? {
coverflowEffect: {
rotate: -56,
stretch: 10,
depth: 80,
scale: 0.6,
modifier: 1,
slideShadows: true,
},
} : {})}
onAfterInit={handleAfterInit}
onSlideChange={handleSlideChange}
className="w-full"
>
{videos.map((src, index) => (
<SwiperSlide key={src} className="select-none">
<div data-alt="video-card" className={`${isMobile ? 'w-screen' : 'w-[48vw]'} mx-auto aspect-video overflow-hidden rounded-xl shadow-lg`}>
<video
data-alt="video"
ref={(el) => { videoRefs.current[index] = el; }}
src={src}
className="h-full w-full object-cover"
muted
playsInline
loop
preload="metadata"
onLoadedData={() => {
// 进入视口/初始化后,确保当前是激活项就播放
const sw = swiperRef.current;
if (!sw) return;
const isActive = (sw.realIndex ?? sw.activeIndex ?? 0) === index;
if (isActive) {
videoRefs.current[index]?.play().catch(() => {});
}
}}
/>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
);
};
export default React.memo(VideoCoverflow);