| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- import { View, Swiper, SwiperItem, Text } from '@tarojs/components'
- import { useState, useEffect, useRef } from 'react'
- import { cn } from '@/utils/cn'
- import { Image } from './image'
- import Taro from '@tarojs/taro'
- export interface CarouselItem {
- /** 图片地址 */
- src: string
- /** 图片标题 */
- title?: string
- /** 图片描述 */
- description?: string
- /** 点击跳转链接 */
- link?: string
- /** 自定义样式 */
- className?: string
- }
- export interface CarouselProps {
- /** 轮播图数据 */
- items: CarouselItem[]
- /** 是否自动播放 */
- autoplay?: boolean
- /** 自动播放间隔时间(毫秒) */
- interval?: number
- /** 是否显示指示器 */
- showIndicators?: boolean
- /** 指示器位置 */
- indicatorPosition?: 'bottom' | 'top' | 'left' | 'right'
- /** 是否循环播放 */
- circular?: boolean
- /** 图片填充模式 */
- imageMode?: 'scaleToFill' | 'aspectFit' | 'aspectFill' | 'widthFix' | 'top' | 'bottom' | 'center' | 'left' | 'right'
- /** 图片高度 */
- height?: number | string
- /** 圆角大小 */
- rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
- /** 点击轮播图回调 */
- onItemClick?: (item: CarouselItem, index: number) => void
- /** 轮播切换回调 */
- onChange?: (current: number) => void
- /** 自定义样式类 */
- className?: string
- }
- const roundedMap = {
- none: '',
- sm: 'rounded-sm',
- md: 'rounded-md',
- lg: 'rounded-lg',
- xl: 'rounded-xl',
- full: 'rounded-full'
- }
- const indicatorPositionMap = {
- bottom: 'bottom-4 left-1/2 -translate-x-1/2',
- top: 'top-4 left-1/2 -translate-x-1/2',
- left: 'left-4 top-1/2 -translate-y-1/2 flex-col',
- right: 'right-4 top-1/2 -translate-y-1/2 flex-col'
- }
- export function Carousel({
- items,
- autoplay = true,
- interval = 3000,
- showIndicators = true,
- indicatorPosition = 'bottom',
- circular = true,
- imageMode = 'aspectFill',
- height = 200,
- rounded = 'md',
- onItemClick,
- onChange,
- className
- }: CarouselProps) {
- const [currentIndex, setCurrentIndex] = useState(0)
- const swiperRef = useRef<any>(null)
- const autoplayTimerRef = useRef<NodeJS.Timeout | null>(null)
- // 处理轮播切换
- const handleChange = (e: any) => {
- const index = e.detail.current
- setCurrentIndex(index)
- onChange?.(index)
- }
- // 处理图片点击
- const handleItemClick = (item: CarouselItem, index: number) => {
- onItemClick?.(item, index)
- }
- // 处理指示器点击
- const handleIndicatorClick = (index: number) => {
- setCurrentIndex(index)
- swiperRef.current?.swiperTo?.(index)
- }
- // 自动播放
- useEffect(() => {
- if (autoplay && items.length > 1) {
- autoplayTimerRef.current = setInterval(() => {
- const nextIndex = (currentIndex + 1) % items.length
- setCurrentIndex(nextIndex)
- swiperRef.current?.swiperTo?.(nextIndex)
- }, interval)
- }
- return () => {
- if (autoplayTimerRef.current) {
- clearInterval(autoplayTimerRef.current)
- }
- }
- }, [autoplay, currentIndex, interval, items.length])
- // 页面隐藏时清除定时器
- useEffect(() => {
- const handleHide = () => {
- if (autoplayTimerRef.current) {
- clearInterval(autoplayTimerRef.current)
- }
- }
- const handleShow = () => {
- if (autoplay && items.length > 1) {
- autoplayTimerRef.current = setInterval(() => {
- const nextIndex = (currentIndex + 1) % items.length
- setCurrentIndex(nextIndex)
- swiperRef.current?.swiperTo?.(nextIndex)
- }, interval)
- }
- }
- // Taro 事件监听
- if (typeof Taro !== 'undefined') {
- Taro.eventCenter.on('onHide', handleHide)
- Taro.eventCenter.on('onShow', handleShow)
- }
- return () => {
- if (typeof Taro !== 'undefined') {
- Taro.eventCenter.off('onHide', handleHide)
- Taro.eventCenter.off('onShow', handleShow)
- }
- }
- }, [autoplay, currentIndex, interval, items.length])
- if (!items || items.length === 0) {
- return (
- <View className={cn('w-full bg-gray-100 flex items-center justify-center', roundedMap[rounded], className)}>
- <View className="text-center">
- <View className="i-heroicons-photo-20-solid w-12 h-12 text-gray-400 mb-2" />
- <Text className="text-gray-500 text-sm">暂无图片</Text>
- </View>
- </View>
- )
- }
- return (
- <View className={cn('relative w-full overflow-hidden', roundedMap[rounded], className)}>
- <Swiper
- ref={swiperRef}
- className="w-full"
- current={currentIndex}
- autoplay={false}
- circular={circular}
- onChange={handleChange}
- style={{ height: typeof height === 'number' ? `${height}rpx` : height }}
- >
- {items.map((item, index) => (
- <SwiperItem key={index} className="w-full h-full">
- <View
- className="w-full h-full relative"
- onClick={() => handleItemClick(item, index)}
- >
- <Image
- src={item.src}
- mode={imageMode}
- className="w-full h-full"
- lazyLoad
- showLoading
- showError
- />
- {item.title && (
- <View className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-4">
- {item.title && (
- <Text className="text-white text-lg font-semibold">{item.title}</Text>
- )}
- {item.description && (
- <Text className="text-white/80 text-sm mt-1">{item.description}</Text>
- )}
- </View>
- )}
- </View>
- </SwiperItem>
- ))}
- </Swiper>
- {/* 指示器 */}
- {showIndicators && items.length > 1 && (
- <View className={cn(
- 'absolute flex items-center gap-2 z-10',
- indicatorPositionMap[indicatorPosition]
- )}>
- {items.map((_, index) => (
- <View
- key={index}
- className={cn(
- 'w-2 h-2 rounded-full transition-all duration-300',
- index === currentIndex
- ? 'bg-white scale-125'
- : 'bg-white/50'
- )}
- onClick={() => handleIndicatorClick(index)}
- />
- ))}
- </View>
- )}
- {/* 左右箭头 - 可选 */}
- {items.length > 1 && (
- <>
- <View
- className={cn(
- 'absolute left-2 top-1/2 -translate-y-1/2 z-10',
- 'w-8 h-8 bg-black/30 rounded-full flex items-center justify-center',
- 'opacity-0 group-hover:opacity-100 transition-opacity'
- )}
- onClick={() => {
- const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1
- setCurrentIndex(prevIndex)
- swiperRef.current?.swiperTo?.(prevIndex)
- }}
- >
- <View className="i-heroicons-chevron-left-20-solid text-white w-4 h-4" />
- </View>
- <View
- className={cn(
- 'absolute right-2 top-1/2 -translate-y-1/2 z-10',
- 'w-8 h-8 bg-black/30 rounded-full flex items-center justify-center',
- 'opacity-0 group-hover:opacity-100 transition-opacity'
- )}
- onClick={() => {
- const nextIndex = (currentIndex + 1) % items.length
- setCurrentIndex(nextIndex)
- swiperRef.current?.swiperTo?.(nextIndex)
- }}
- >
- <View className="i-heroicons-chevron-right-20-solid text-white w-4 h-4" />
- </View>
- </>
- )}
- </View>
- )
- }
|