This commit is contained in:
2025-10-27 19:05:56 +08:00
parent 0033ac10ec
commit 98c73632bd
25 changed files with 1223 additions and 133 deletions

View File

@@ -0,0 +1,267 @@
<template>
<div class="custom-carousel">
<div class="carousel-container">
<!-- 轮播内容 -->
<div
class="carousel-track"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
>
<div
v-for="(item, index) in items"
:key="index"
class="carousel-item"
>
<slot :item="item" :index="index"></slot>
</div>
</div>
<!-- 左右箭头 -->
<button
v-if="showArrows"
class="carousel-arrow carousel-arrow-left"
@click="prev"
:disabled="currentIndex === 0 && !loop"
>
<el-icon><ArrowLeft /></el-icon>
</button>
<button
v-if="showArrows"
class="carousel-arrow carousel-arrow-right"
@click="next"
:disabled="currentIndex === items.length - 1 && !loop"
>
<el-icon><ArrowRight /></el-icon>
</button>
</div>
<!-- 指示器 -->
<div
v-if="showIndicators"
class="carousel-indicators"
:class="indicatorPosition"
>
<button
v-for="(item, index) in items"
:key="index"
class="indicator-item"
:class="{ active: currentIndex === index }"
@click="goTo(index)"
>
<img
v-if="currentIndex === index && activeIcon"
:src="activeIcon"
class="indicator-icon"
/>
<span v-else class="indicator-dot"></span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
interface Props {
items: any[];
autoplay?: boolean;
interval?: number;
loop?: boolean;
showArrows?: boolean;
showIndicators?: boolean;
indicatorPosition?: 'bottom-center' | 'bottom-right' | 'bottom-left';
activeIcon?: string;
inactiveIcon?: string;
}
const props = withDefaults(defineProps<Props>(), {
autoplay: true,
interval: 5000,
loop: true,
showArrows: true,
showIndicators: true,
indicatorPosition: 'bottom-right',
});
const currentIndex = ref(0);
let timer: ReturnType<typeof setInterval> | null = null;
function next() {
if (currentIndex.value < props.items.length - 1) {
currentIndex.value++;
} else if (props.loop) {
currentIndex.value = 0;
}
}
function prev() {
if (currentIndex.value > 0) {
currentIndex.value--;
} else if (props.loop) {
currentIndex.value = props.items.length - 1;
}
}
function goTo(index: number) {
currentIndex.value = index;
resetTimer();
}
function startAutoplay() {
if (props.autoplay) {
timer = setInterval(() => {
next();
}, props.interval);
}
}
function stopAutoplay() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function resetTimer() {
stopAutoplay();
startAutoplay();
}
onMounted(() => {
startAutoplay();
});
onUnmounted(() => {
stopAutoplay();
});
defineExpose({
next,
prev,
goTo,
});
</script>
<style lang="scss" scoped>
.custom-carousel {
width: 100%;
height: 100%;
position: relative;
.carousel-container {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
.carousel-track {
display: flex;
height: 100%;
transition: transform 0.5s ease-in-out;
.carousel-item {
min-width: 100%;
height: 100%;
flex-shrink: 0;
}
}
.carousel-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(6.4px);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
z-index: 10;
color: #FFFFFF;
&:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.5);
}
&:disabled {
opacity: 0.3;
cursor: not-allowed;
}
&.carousel-arrow-left {
left: 20px;
}
&.carousel-arrow-right {
right: 20px;
}
.el-icon {
font-size: 20px;
}
}
}
.carousel-indicators {
display: flex;
gap: 13px;
position: absolute;
z-index: 10;
&.bottom-center {
bottom: 32px;
left: 50%;
transform: translateX(-50%);
}
&.bottom-right {
bottom: 32px;
right: 120px;
}
&.bottom-left {
bottom: 32px;
left: 120px;
}
.indicator-item {
width: 22px;
height: 22px;
border: none;
background: transparent;
cursor: pointer;
padding: 0;
transition: all 0.3s;
.indicator-dot {
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(191, 191, 191, 0.5);
backdrop-filter: blur(6.4px);
}
.indicator-icon {
width: 100%;
height: 100%;
object-fit: contain;
}
&:hover .indicator-dot {
background: rgba(255, 255, 255, 0.6);
}
&.active .indicator-dot {
background: rgba(255, 255, 255, 0.8);
}
}
}
}
</style>

View File

@@ -1,4 +1,5 @@
export { default as Breadcrumb } from './Breadcrumb.vue';
export { default as Carousel } from './Carousel.vue';
export { default as FloatingSidebar } from './FloatingSidebar.vue';
export { default as MenuItem } from './MenuItem.vue';
export { default as MenuNav } from './MenuNav.vue';