forked from 77media/video-flow
178 lines
7.2 KiB
TypeScript
178 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
|
|
-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'}
|
|
data-test={isMobile}
|
|
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 py-8"
|
|
>
|
|
{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);
|
|
|
|
|