carousel.vue
<template>
<div class="uui-carousel" :style="getStyle" @mouseenter="handleStop" @mouseleave="handleContinue">
<ul class="carousel-body">
<li v-for="(item, i) in props.data" :key="i" class="carousel-item" :class="{ fade: i === index }">
<img :src="item" :style="imgStyle">
</li>
</ul>
<div class="carousel-btn prev" @click="handleToggleFN(-1)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="28" height="28"><path fill="#ffffff" d="M10.26 3.2a.75.75 0 0 1 .04 1.06L6.773 8l3.527 3.74a.75.75 0 1 1-1.1 1.02l-4-4.25a.75.75 0 0 1 0-1.02l4-4.25a.75.75 0 0 1 1.06-.04z" /></svg>
</div>
<div class="carousel-btn next" @click="handleToggleFN(1)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="28" height="28"><path fill="#ffffff" d="M5.74 3.2a.75.75 0 0 0-.04 1.06L9.227 8L5.7 11.74a.75.75 0 1 0 1.1 1.02l4-4.25a.75.75 0 0 0 0-1.02l-4-4.25a.75.75 0 0 0-1.06-.04z" /></svg>
</div>
<div class="carousel-indicator">
<span v-for="i in props.data.length" :key="i" :class="{ active: index === i - 1 }" @click="index = i - 1" />
</div>
</div>
</template>
<script setup lang="ts">
import './less/app.less'
const props = withDefaults(
defineProps<{
data: string[],
duration?: number,
autoplay?: boolean,
height?: string,
width?: string,
fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
}>(), {
duration: 2,
autoplay: true,
height: '380px',
width: '100%',
fit: 'cover'
}
)
const getStyle = ref('height:' + props.height)
const imgStyle = ref('object-fit:' + props.fit)
const index = ref(0)
const timer = ref<NodeJS.Timeout>()
const handleAutoplay = () => {
clearInterval(timer.value)
timer.value = setInterval(() => {
index.value++
if (index.value >= props.data.length) index.value = 0
}, props.duration * 1000)
}
const handleStop = () => {
if (timer.value) clearInterval(timer.value)
}
const handleContinue = () => {
if (props.data.length > 1 && props.autoplay) handleAutoplay()
}
const handleToggleFN = (num: number) => {
index.value += num
if (index.value >= props.data.length) return (index.value = 0)
if (index.value < 0) index.value = props.data.length - 1
}
watch(() => props.data, () => {
if (props.data.length > 1 && props.autoplay) {
index.value = 0
handleAutoplay()
}
}, { immediate: true })
onUnmounted(() => {
if (timer.value) clearInterval(timer.value)
})
</script>
app.less
.uui-carousel {
position: relative;
&:hover {
.carousel-btn {
opacity: 1;
}
}
.carousel-body {
width: 100%;
height: 100%;
.carousel-item {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0;
transition: opacity 0.5s linear;
&.fade {
opacity: 1;
z-index: 1;
}
img {
width: 100%;
height: 100%;
}
}
}
.carousel-btn {
width: 38px;
height: 38px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.2);
color: #fff;
border-radius: 50%;
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 2;
text-align: center;
opacity: 0;
transition: all 0.3s;
&:hover {
opacity: 0.7;
}
&.prev {
left: 20px;
}
&.next {
right: 20px;
}
}
.carousel-indicator {
position: absolute;
left: 0;
bottom: 16px;
z-index: 2;
width: 100%;
text-align: center;
span {
width: 12px;
height: 12px;
display: inline-block;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 50%;
margin: 0 4px;
cursor: pointer;
transition: background-color .3s cubic-bezier(.4, 0, .2, 1);
&.active {
background: var(--uui-theme);
}
}
}
}
使用方式
<template>
<Carousel :data="data" />
</template>
<script setup lang="ts">
const data = [
'https://picsum.photos/id/11/300/300',
'https://picsum.photos/id/22/800/800',
'https://picsum.photos/id/33/900/900',
'https://picsum.photos/id/44/500/500',
'https://picsum.photos/id/55/600/600'
]
</script>