Carousel 走马灯
轮播组件,用于循环播放一组图片或卡片。
Carousel 源码
vue
<script lang="ts" setup>
import { onMounted, ref, onScopeDispose, watch } from 'vue';
import { changeCarousel, throttle } from '@stao-ui/utils';
import IconArrowLeft from '../icons/IconArrowLeft.vue';
import IconArrowRight from '../icons/IconArrowRight.vue';
defineOptions({
name: 'BaseCarousel'
});
const $props = withDefaults(
defineProps<{
direction?: 'horizontal' | 'vertical';
autoPlay?: boolean;
interval?: number;
duration?: number;
initialIndex?: number;
loop?: boolean;
dotPosition?: 'top' | 'bottom' | 'left' | 'right';
showDot?: boolean;
easing?: string;
arrow?: 'always' | 'hover' | 'never';
beforeChange?: (from: number, to: number) => void;
afterChange?: (current: number) => void;
}>(),
{
direction: 'horizontal',
autoPlay: false,
interval: 3000,
loop: true,
dotPosition: 'bottom',
showDot: true,
duration: 300,
easing: 'ease',
initialIndex: 0,
arrow: 'hover'
}
);
const changeIndex = (index: number, total: number, loop = true) => {
if (index < 0) {
return loop ? total - 1 : 0;
} else if (index > total - 1) {
return loop ? 0 : total - 1;
} else {
return index;
}
};
let timer: NodeJS.Timer | null = null;
onScopeDispose(() => {
timer && clearInterval(timer);
});
const activeIndex = ref<number>(0);
const total = ref<number>(0);
const carouselRef = ref<HTMLElement>();
const autoplayCarousel = () => {
if (!$props.autoPlay) {
return;
}
timer && clearInterval(timer);
timer = setInterval(() => {
const idx = changeIndex(activeIndex.value + 1, total.value, $props.loop);
$props.beforeChange?.(activeIndex.value, idx);
// stop autoplay, when index is not changed
if (idx === activeIndex.value) {
clearInterval(timer!);
return;
}
activeIndex.value = idx;
changeCarousel({
index: activeIndex.value,
el: carouselRef.value!,
timingFunc: $props.easing,
duration: $props.duration,
direction: $props.direction,
afterChange: $props.afterChange
});
}, $props.interval);
};
const init = () => {
const element = carouselRef.value;
if (!element) {
return;
}
total.value = element.children.length;
if (!total.value) {
return;
}
activeIndex.value = $props.initialIndex;
// set default index
if (activeIndex.value) {
changeCarousel({
index: activeIndex.value,
el: element,
duration: 0,
direction: $props.direction
});
}
autoplayCarousel();
};
onMounted(() => {
init();
});
watch(
() => $props,
() => {
timer && clearInterval(timer);
init();
},
{ deep: true }
);
const onGoTo = throttle<number>((index: number) => {
timer && clearInterval(timer);
const idx = changeIndex(index, total.value, $props.loop);
$props.beforeChange?.(activeIndex.value, idx);
activeIndex.value = idx;
changeCarousel({
index: activeIndex.value,
el: carouselRef.value!,
timingFunc: $props.easing,
duration: $props.duration,
direction: $props.direction,
afterChange: () => {
$props.afterChange?.(activeIndex.value);
autoplayCarousel();
}
});
}, $props.duration);
const onPrev = () => {
onGoTo(activeIndex.value - 1);
};
const onNext = () => {
onGoTo(activeIndex.value + 1);
};
defineExpose({
onGoTo,
onPrev,
onNext
});
</script>
<template>
<div class="base-carousel">
<div
ref="carouselRef"
class="base-carousel-wrapper"
:class="{ flex: direction === 'horizontal' }">
<slot />
</div>
<ul
v-if="showDot"
class="base-carousel-dot"
:class="{
'base-carousel-dot--bottom base-carousel-dot--h': dotPosition === 'bottom',
'base-carousel-dot--top base-carousel-dot--h': dotPosition === 'top',
'base-carousel-dot--left base-carousel-dot--v': dotPosition === 'left',
'base-carousel-dot--right base-carousel-dot--v': dotPosition === 'right'
}">
<li v-for="(item, index) in total" :key="item">
<slot name="dot" :active="activeIndex === index" :index="index">
<button
class="base-carousel-dot-item"
:class="{ 'dot-item--active': activeIndex === index }"
@click="onGoTo(index)"></button>
</slot>
</li>
</ul>
<div
class="base-arrow base-arrow--left"
:class="{ 'base-arrow--always': arrow === 'always' }"
v-if="arrow !== 'never'">
<slot name="arrowLeft">
<button class="arrow-button" @click="onPrev">
<IconArrowLeft />
</button>
</slot>
</div>
<div
class="base-arrow base-arrow--right"
:class="{ 'base-arrow--always': arrow === 'always' }"
v-if="arrow !== 'never'">
<slot name="arrowRight">
<button class="arrow-button" @click="onNext">
<IconArrowRight />
</button>
</slot>
</div>
</div>
</template>
<style lang="scss" scoped>
.flex {
display: flex;
}
.base-carousel {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
.base-carousel-wrapper {
height: 100%;
}
.base-arrow {
position: absolute;
top: 50%;
opacity: 0;
transform: translateY(-50%);
transition: all 0.3s ease;
&.base-arrow--always {
opacity: 1;
}
}
&:hover .base-arrow {
opacity: 1;
}
}
.base-arrow--left {
left: 15px;
}
.base-arrow--right {
right: 15px;
}
.arrow-button--none {
opacity: 0;
}
.arrow-button {
all: unset;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.3);
color: #fff;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover,
&:focus {
background-color: rgba(0, 0, 0, 0.6);
}
}
.base-carousel-dot {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
z-index: 9;
list-style: none;
li {
margin: 0;
padding: 0;
}
}
.base-carousel-dot--h {
left: 50%;
transform: translateX(-50%);
}
.base-carousel-dot--v {
flex-direction: column;
top: 50%;
transform: translateY(-50%);
}
.base-carousel-dot--top {
top: 15px;
}
.base-carousel-dot--bottom {
bottom: 15px;
}
.base-carousel-dot--left {
left: 15px;
}
.base-carousel-dot--right {
right: 15px;
}
.base-carousel-dot-item {
all: unset;
display: block;
opacity: 0.3;
width: 8px;
height: 8px;
margin: 4px;
border-radius: 50%;
background-color: #fff;
transition: all 0.3s ease;
cursor: pointer;
&:hover,
&:focus {
opacity: 0.7;
}
&.dot-item--active {
opacity: 1;
&:hover,
&:focus {
opacity: 1;
}
}
}
</style>CarouselItem 源码
vue
<script lang="ts" setup>
defineOptions({
name: 'BaseCarouselItem'
})
</script>
<template>
<div class="base-carousel-item"><slot /></div>
</template>
<style lang="scss" scoped>
.base-carousel-item {
flex-shrink: 0;
width: 100%;
height: 100%;
}
</style>基本用法
水平轮播(默认):垂直轮播(自动):
1
2
3
1
2
3
示例代码
vue
<template>
<div class="container">
<span class="text">水平轮播(默认):</span>
<BaseCarousel>
<BaseCarouselItem>
<div class="item item-1">1</div>
</BaseCarouselItem>
<BaseCarouselItem>
<div class="item item-2">2</div>
</BaseCarouselItem>
<BaseCarouselItem>
<div class="item item-3">3</div>
</BaseCarouselItem>
</BaseCarousel>
<span class="text">垂直轮播(自动):</span>
<BaseCarousel direction="vertical" dot-position="left" arrow="never" auto-play>
<BaseCarouselItem>
<div class="item item-1">1</div>
</BaseCarouselItem>
<BaseCarouselItem>
<div class="item item-2">2</div>
</BaseCarouselItem>
<BaseCarouselItem>
<div class="item item-3">3</div>
</BaseCarouselItem>
</BaseCarousel>
</div>
</template>
<script lang="ts" setup>
import BaseCarousel from '@/SCarousel/index.vue';
import BaseCarouselItem from '@/SCarouselItem/index.vue';
</script>
<style lang="scss" scoped>
.container {
display: flex;
flex-direction: column;
gap: 14px;
height: 400px;
.item {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #fff;
font-size: 24px;
}
.item-1 {
background-color: rgb(214, 103, 103);
}
.item-2 {
background-color: rgb(122, 170, 122);
}
.item-3 {
background-color: rgb(200, 171, 5);
}
}
</style>API
Carousel Props
| 参数名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| direction | 轮播方向 | 'horizontal' | 'vertical' | horizontal |
| autoPlay | 是否自动轮播 | boolean | false |
| interval | 自动轮播间隔,单位为毫秒 | number | 3000 |
| duration | 动画时长,单位为毫秒 | number | 300 |
| initialIndex | 初始状态激活的轮播项的索引 | number | 0 |
| showDot | 是否显示指示点 | boolean | true |
| loop | 是否循环播放 | boolean | true |
| dotPosition | 指示器位置 | 'top' | 'bottom' | 'left' | 'right' | bottom |
| arrow | 箭头触发方式 | 'always' | 'hover' | 'never' | always |
| beforeChange | 索引改变前回调函数 | (from: number, to: number) => void | - |
| afterChange | 索引改变后回调函数 | (index: number) => void | - |
Carousel Methods
| 方法名 | 说明 | 参数 |
|---|---|---|
| onGoTo | 跳转到指定索引 | index: number |
| onPrev | 跳转到上一个索引 | - |
| onNext | 跳转到下一个索引 | - |
Carousel Slots
| 插槽名 | 说明 | 参数 |
|---|---|---|
| default | 需要显示的内容 | - |
| dot | 自定义指示点 | {active: boolean; index: number} |
| arrowLeft | 自定义左箭头 | - |
| arrowRight | 自定义右箭头 | - |
CarouselItem Slots
| 插槽名 | 说明 | 参数 |
|---|---|---|
| default | 需要显示的内容 | - |