news/app/components/Carousel.tsx

151 lines
5.5 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 引入 React 核心库
import * as React from "react";
// 引入 Embla Carousel 的自动播放插件
import Autoplay from "embla-carousel-autoplay";
// 引入自定义 UI 组件Card 和 CardContent通常用于内容容器
import { Card, CardContent } from "@/ui/card";
// 引入自定义轮播组件及其子组件和类型定义
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi, // Embla 轮播实例的类型定义
} from "@/ui/carousel";
// 定义轮播图使用的图片资源路径数组
const imageUrls = [
"/images/carousel-1.jpg",
"/images/carousel-2.jpg",
"/images/carousel-3.jpg",
"/images/carousel-4.jpg",
"/images/carousel-5.jpg",
"/images/carousel-6.jpg",
];
// 定义组件接收的 props 接口,提供类型安全
export interface CarouselDemoProps {
// 分页指示器位置:'left' 表示左下角,'right' 表示右下角,默认为 'right'
paginationPosition?: 'left' | 'right';
// 分页指示器样式:'dot' 为圆形小点,'bar' 为横向小条,默认为 'dot'
paginationStyle?: 'dot' | 'bar';
}
// 导出 CarouselDemo 组件,接收两个可选 props并设置默认值
export function CarouselDemo({
paginationPosition = 'right', // 默认右下角
paginationStyle = 'dot', // 默认圆形指示器
}: CarouselDemoProps) {
// 存储 Embla 轮播实例的引用,用于控制滚动等操作
const [api, setApi] = React.useState<CarouselApi>();
// 当前激活的幻灯片索引(从 0 开始)
const [current, setCurrent] = React.useState(0);
// 总共的幻灯片数量(由 Embla API 动态获取)
const [count, setCount] = React.useState(0);
// 图片总数(用于渲染轮播项)
const totalSlides = imageUrls.length;
// 副作用:监听 api 变化,初始化轮播状态并绑定 select 事件
React.useEffect(() => {
if (!api) return; // 如果 api 尚未就绪,直接返回
// 获取所有 snap 点的数量(即幻灯片总数)
setCount(api.scrollSnapList().length);
// 设置当前选中的 snap 索引
setCurrent(api.selectedScrollSnap());
// 监听轮播切换事件(用户手动滑动或自动播放时触发)
api.on("select", () => {
setCurrent(api.selectedScrollSnap()); // 更新当前激活项
});
}, [api]); // 仅当 api 发生变化时重新执行
// 渲染组件
return (
// 外层容器:相对定位,占满父容器宽高
<div className="relative w-full h-full">
{/* 轮播主容器 */}
<Carousel
opts={{
loop: true, // 启用循环播放(最后一张后回到第一张)
}}
plugins={[
// 配置自动播放插件:每 3 秒切换一次,且用户交互时不暂停
Autoplay({
delay: 3000,
stopOnInteraction: false,
}),
]}
setApi={setApi} // Embla 实例保存到 state
className="w-full h-full" // 占满容器
>
{/* 轮播内容区域 */}
<CarouselContent className="h-full w-full -ml-0">
{/* 遍历图片数组,为每张图创建一个轮播项 */}
{imageUrls.map((src, index) => (
<CarouselItem
key={index}
className="w-full h-full pl-0" // 移除默认内边距
>
{/* 内部包裹层:无内边距,占满 */}
<div className="p-0 w-full h-full">
{/* 使用 CardContent 包裹图片,移除默认间距 */}
<CardContent className="flex items-center justify-center p-0 w-full h-full m-0">
{/* 图片元素:自动填充容器,保持比例裁剪 */}
<img
src={src}
alt={`Slide ${index + 1}`} // 无障碍访问描述
className="w-full h-full object-fill" // 关键使图片覆盖整个区域
loading="lazy" // 懒加载优化性能
/>
</CardContent>
</div>
</CarouselItem>
))}
</CarouselContent>
{/* 上一张按钮(左箭头) */}
{/* <CarouselPrevious className="absolute left-2 top-1/2 -translate-y-1/2 z-10" /> */}
{/* 下一张按钮(右箭头) */}
{/* <CarouselNext className="absolute right-2 top-1/2 -translate-y-1/2 z-10" /> */}
</Carousel>
{/* 分页指示器容器:绝对定位在底部 */}
<div
className={`absolute bottom-4 ${
paginationPosition === 'left' ? 'left-4' : 'right-4' // 根据 prop 控制左右位置
} flex gap-2 z-10`} // 水平排列间距 0.5rem置于轮播图上方
>
{/* 动态生成指示器按钮 */}
{Array.from({ length: count }).map((_, index) => (
<button
key={index}
onClick={() => api?.scrollTo(index)} // 点击跳转到对应幻灯片
className={`transition-colors ${
// 根据 paginationStyle 决定形状
paginationStyle === 'dot'
? 'h-2 w-2 rounded-full' // 圆形:宽高相等 + 全圆角
: 'h-2 w-8 rounded' // 块状:宽 2rem高 0.5rem,带圆角
} ${
index === current
? 'bg-white' // 当前项为纯白色
: 'bg-white/50' // 非当前项为半透明白色
}`}
aria-label={`Go to slide ${index + 1}`} // 无障碍支持
/>
))}
</div>
</div>
);
}