Carousel 轮播图
轮播图组件,常用于一组图片轮播。
代码演示
基本使用
默认
interval=2000 throttleTime=500 defaultActiveIndex=2
autoPlay = false throttleTime=1000
import React from 'react'
import { Space, Carousel } from '@dance-ui/ui'
export default () => (
<Space justify="center" align="center" style={{ width: '100%' }}>
<div>
<h4>默认 </h4>
<Carousel width={300} height={200}>
<img src="https://fakeimg.pl/300x200/ff0000/?text=1" width={300} height={200} />
<img src="https://fakeimg.pl/300x200/0000ff/?text=2" width={300} height={200} />
<img src="https://fakeimg.pl/300x200/00ff00/?text=3" width={300} height={200} />
</Carousel>
</div>
<div>
<h4>
interval={2000} throttleTime={500} defaultActiveIndex={2}
</h4>
<Carousel width={360} height={250} interval={2000} throttleTime={1000} defaultActiveIndex={2}>
<img src="https://fakeimg.pl/360x250/ff0000/?text=1" width={360} height={250} />
<img src="https://fakeimg.pl/360x250/0000ff/?text=2" width={360} height={250} />
<img src="https://fakeimg.pl/360x250/00ff00/?text=3" width={360} height={250} />
<img src="https://fakeimg.pl/360x250/ffff00/?text=4" width={360} height={250} />
<img src="https://fakeimg.pl/360x250/ff00ff/?text=5" width={360} height={250} />
</Carousel>
</div>
<div>
<h4>autoPlay = false throttleTime={1000}</h4>
<Carousel width={360} height={250} throttleTime={1000} autoplay={false}>
<img src="https://fakeimg.pl/360x250/ff0000/?text=1" width={360} height={250} />
<img src="https://fakeimg.pl/360x250/0000ff/?text=2" width={360} height={250} />
<img src="https://fakeimg.pl/360x250/00ff00/?text=3" width={360} height={250} />
<img src="https://fakeimg.pl/360x250/ffff00/?text=4" width={360} height={250} />
<img src="https://fakeimg.pl/360x250/ff00ff/?text=5" width={360} height={250} />
</Carousel>
</div>
</Space>
)
API
属性 | 默认值 | 说明 | 类型 | 默认值 |
---|---|---|---|---|
width | 必选 | 轮播图宽度 | number | - |
height | 必选 | 轮播图高度 | number | - |
defaultActiveIndex | 可选 | 默认轮播第几张图 | number | 1 |
interval | 可选 | 轮播图自动切换间隔,单位为 ms | number | 1000 |
transitionTime | 可选 | 轮播图切换所需时间,单位为 s | number | 0.5 |
throttleTime | 可选 | 轮播图事件节流时间,单位为 ms | number | 300 |
renderLeftArrow | 可选 | 渲染左侧指示器,preEvent 为切换上一张图的事件 | ({ preEvent }: { preEvent: () => void }) => React.ReactNode | - |
renderRightArrow | 可选 | 渲染右侧指示器,nextEvent 为切换下一张图的事件 | ({ nextEvent }: { nextEvent: () => void }) => React.ReactNode | - |
dotClass | 可选 | 下方圆点指示器的类名 | string | - |
renderDot | 可选 | 渲染下方圆点指示器,goTo 为切换到第 idx 张图的函数 | ({ goTo }: { goTo: (idx: number) => void }) => React.ReactNode | - |
autoplay | 可选 | 是否自动播放 | boolean | true |
组件源码
组件源码
import classNames from 'classnames'
import _ from 'lodash-es'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
import Icon, { IconType } from '../Icon'
export type CarouselProps = {
children: React.ReactNode
width: number
height: number
// 默认位于第几张图
defaultActiveIndex?: number
// 轮播图 自动轮播间隔 单位ms
interval?: number
// CSS transition 时间 单位为s
transitionTime?: number
// 自动播放
autoplay?: boolean
// 渲染上一个指示器
renderLeftArrow?: ({ preEvent }: { preEvent: () => void }) => React.ReactNode
// 渲染下一个指示器
renderRightArrow?: ({ nextEvent }: { nextEvent: () => void }) => React.ReactNode
// 按钮throttle时间
throttleTime?: number
// 渲染圆点指示器
dotClass?: string
renderDot?: ({ goTo }: { goTo: (idx: number) => void }) => React.ReactNode
}
type CarouselStyledProps = {
offset: number
enableTransitionAnim?: boolean
transitionTime?: number
}
const CarouselWrapper = styled.div`
transform: ${({ offset }: CarouselStyledProps) => `translateX(${offset}px)`};
transition: ${({ enableTransitionAnim, transitionTime }: CarouselStyledProps) =>
enableTransitionAnim ? `all ${transitionTime ?? 0.5}s` : 'none'};
`
// TODO: new package utils and push
function useThrottle(fn: any, delay: number) {
const options = { leading: true, trailing: false } // add custom lodash options
const fnRef = useRef(fn)
// use mutable ref to make useCallback/throttle not depend on `fn` dep
useEffect(() => {
fnRef.current = fn
})
// eslint-disable-next-line react-hooks/exhaustive-deps
return useCallback(
_.throttle((...args) => fnRef.current(...args), delay, options),
[delay],
)
}
// TODO:mvp版本 优化offset、width
const Carousel = (props: CarouselProps): JSX.Element => {
const {
defaultActiveIndex,
children,
transitionTime,
width,
height,
interval,
renderLeftArrow,
renderRightArrow,
throttleTime,
dotClass,
renderDot,
autoplay,
} = props
const [index, setIndex] = useState(defaultActiveIndex ?? 1)
const [enableTransitionAnim, setEnableTransitionAnim] = useState(true)
const length = React.Children.count(children)
const realLength = length ? length + 2 : 0
const offset = useMemo(() => -width * index, [index, width]) // 计算偏移量
const timer = useRef<any>(null)
const [isMoving, setIsMoving] = useState(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleChange = (idx: number) => {
// 当在动画进行时,不允许切换
if (!realLength || isMoving) return
// 切换前先把动画参数打开
setEnableTransitionAnim(true)
// 修改状态为进行时
setIsMoving(true)
// 改变当前位置
setIndex(idx)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleChangeWithoutAnim = (idx: number) => {
// 当在动画进行时,不允许切换
if (!realLength || isMoving) return
// 切换前先把动画参数打开
setEnableTransitionAnim(false)
// 改变当前位置
setIndex(idx)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const nextItem = () => {
const tar = index === realLength - 1 ? 0 : index + 1
handleChange(tar)
}
const preItem = () => {
handleChange(index === 0 ? realLength - 1 : index - 1)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const setTimer = () =>
setTimeout(() => {
nextItem()
}, interval)
// 定时器自动播放
useEffect(() => {
if (!autoplay) return
timer.current && clearTimeout(timer.current)
timer.current = setTimer()
return () => {
timer.current && clearTimeout(timer.current)
}
}, [autoplay, interval, setTimer])
useEffect(() => {
if (isMoving) return
if (index === realLength - 1) {
handleChangeWithoutAnim(1)
} else if (index === 0) {
handleChangeWithoutAnim(realLength - 2)
}
}, [handleChangeWithoutAnim, index, isMoving, realLength])
// 处理过渡结束事件
const handleTransitionEnd = () => {
// 修改状态为完成
setIsMoving(false)
}
const throttledPreItem = useThrottle(preItem, throttleTime ?? 300)
const throttledNextItem = useThrottle(nextItem, throttleTime ?? 300)
const throttledHandleChange = useThrottle(handleChange, throttleTime ?? 300)
// 渲染轮播图列表
const renderList = () => {
if (!length) return null
const childrenArray = React.Children.toArray(children)
const firstElement = childrenArray[0]
const lastElement = childrenArray[length - 1]
childrenArray.unshift(lastElement)
childrenArray.push(firstElement)
return childrenArray.map((item, i) => (
<div key={i} style={{ width, height }}>
{item}
</div>
))
}
const _renderLeftArrow = () => {
if (renderLeftArrow && typeof renderLeftArrow === 'function') return renderLeftArrow({ preEvent: throttledPreItem })
return (
<div
className="absolute inset-y-0 left-0 flex cursor-pointer items-center justify-center bg-black/10 p-2"
onClick={throttledPreItem}>
<Icon type={IconType.ARROW} className="h-5 w-5 rotate-180 fill-white" />
</div>
)
}
const _renderRightArrow = () => {
if (renderRightArrow && typeof renderRightArrow === 'function') return renderRightArrow({ nextEvent: throttledNextItem })
return (
<div
className="absolute inset-y-0 right-0 flex cursor-pointer items-center justify-center bg-black/10 p-2"
onClick={throttledNextItem}>
<Icon type={IconType.ARROW} className="h-5 w-5 fill-white" />
</div>
)
}
const _renderDot = () => {
if (renderDot && typeof renderDot === 'function') return renderDot({ goTo: throttledHandleChange })
if (!length) return null
const dots = []
for (let i = 0; i < length; ++i) {
dots.push(
<div
key={i}
className={classNames('aspect-square w-2 cursor-pointer rounded-full bg-white/90', dotClass)}
onClick={() => throttledHandleChange(i + 1)}></div>,
)
}
return (
<div className="absolute inset-x-0 bottom-0 flex items-center justify-center">
<div className="mx-auto mb-1 flex items-center justify-center gap-2 rounded-full bg-black/10 p-2">
{dots.map((v) => v)}
</div>
</div>
)
}
return (
<div className="relative flex overflow-hidden" style={{ width, height }}>
<CarouselWrapper
className="flex"
offset={offset}
transitionTime={transitionTime}
enableTransitionAnim={enableTransitionAnim}
onTransitionEnd={handleTransitionEnd}>
{renderList()}
</CarouselWrapper>
{_renderLeftArrow()}
{_renderRightArrow()}
{_renderDot()}
</div>
)
}
Carousel.defaultProps = {
defaultActiveIndex: 1,
interval: 1000,
transitionTime: 0.5,
throttleTime: 300,
autoplay: true,
}
export default Carousel