detail page port
This commit is contained in:
173
components/Slider.vue
Normal file
173
components/Slider.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="slider">
|
||||
<div class="slider-container" ref="emblaNode">
|
||||
<div class="slides">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="controls" class="controls">
|
||||
<button @click="onPrev">
|
||||
<arrow />
|
||||
</button>
|
||||
<button @click="onNext">
|
||||
<arrow />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="nav && slideCount > 1" class="nav">
|
||||
<btn
|
||||
v-for="i in slideCount"
|
||||
@click="onNavClick(i)"
|
||||
:secondary="activeIndex !== i"
|
||||
number
|
||||
>{{ i }}</btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import Btn from '@/components/Btn.vue'
|
||||
import Arrow from '@/components/svg/util/Arrow.vue'
|
||||
import embla from 'embla-carousel-vue'
|
||||
import ClassNames from 'embla-carousel-class-names'
|
||||
import Autoplay from 'embla-carousel-autoplay'
|
||||
|
||||
const props = defineProps({
|
||||
emblaOptions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
autoplayOptions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
controls: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
nav: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
slideCount: {
|
||||
type: Number,
|
||||
default: () => 0,
|
||||
},
|
||||
})
|
||||
|
||||
const plugins = computed(() => {
|
||||
const base = [ClassNames()]
|
||||
if (props.autoplay) {
|
||||
base.push(Autoplay(props.autoplayOptions))
|
||||
}
|
||||
return base
|
||||
})
|
||||
|
||||
const [emblaNode, emblaApi] = embla(props.emblaOptions, plugins.value)
|
||||
|
||||
defineExpose({
|
||||
api: emblaApi,
|
||||
})
|
||||
|
||||
const activeIndex = ref(1)
|
||||
const onNext = () => {
|
||||
emblaApi.value.scrollNext()
|
||||
}
|
||||
const onPrev = () => {
|
||||
emblaApi.value.scrollPrev()
|
||||
}
|
||||
const onNavClick = (i) => {
|
||||
activeIndex.value = i
|
||||
|
||||
emblaApi.value.scrollTo(i - 1)
|
||||
}
|
||||
|
||||
watch(emblaApi, () => {
|
||||
if (!emblaApi.value) return
|
||||
|
||||
emblaApi.value.on('select', () => {
|
||||
activeIndex.value = emblaApi.value.selectedScrollSnap() + 1
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.slider {
|
||||
position: relative;
|
||||
|
||||
.slider-container {
|
||||
max-width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
.slides {
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--layout-margin);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
|
||||
button {
|
||||
pointer-events: all;
|
||||
border-radius: 50%;
|
||||
background: var(--black);
|
||||
color: var(--white);
|
||||
width: desktop-vw(40px);
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: desktop-vw(21px);
|
||||
height: auto;
|
||||
}
|
||||
&:first-child {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
button {
|
||||
width: mobile-vw(32px);
|
||||
|
||||
svg {
|
||||
width: mobile-vw(16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: desktop-vw(25px);
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@include mobile {
|
||||
bottom: mobile-vw(25px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user