19 changed files with 337 additions and 178 deletions
@ -0,0 +1,57 @@ |
|||||||
|
<template> |
||||||
|
<ClientOnly> |
||||||
|
<div class="swiper-card-container"> |
||||||
|
<swiper-container ref="containerRef" :init="false" :effect="'coverflow'"> |
||||||
|
<slot></slot> |
||||||
|
</swiper-container> |
||||||
|
<div class="swiper-cards-prev"></div> |
||||||
|
<div class="swiper-cards-next"></div> |
||||||
|
</div> |
||||||
|
</ClientOnly> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup> |
||||||
|
const containerRef = ref(null) |
||||||
|
useSwiper(containerRef, { |
||||||
|
effect: 'cards', |
||||||
|
grabCursor: true, |
||||||
|
centeredSlides: true, |
||||||
|
loop: true, |
||||||
|
slidesPerView: 'auto', |
||||||
|
cardsEffect: { |
||||||
|
slideShadows: true, |
||||||
|
perSlideOffset: 8, |
||||||
|
}, |
||||||
|
|
||||||
|
pagination: { |
||||||
|
el: '.swiper-pagination', |
||||||
|
clickable: true, |
||||||
|
}, |
||||||
|
navigation: { |
||||||
|
nextEl: '.swiper-cards-next', |
||||||
|
prevEl: '.swiper-cards-prev', |
||||||
|
}, |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.swiper-card-container { |
||||||
|
width: 100%; |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
/* .swiper-cards-prev, |
||||||
|
.swiper-cards-next { |
||||||
|
@apply absolute top-0 bottom-0 w-32 z-10; |
||||||
|
} |
||||||
|
|
||||||
|
.swiper-cards-prev { |
||||||
|
left: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
.swiper-cards-next { |
||||||
|
right: 0px; |
||||||
|
} */ |
||||||
|
</style> |
@ -0,0 +1,116 @@ |
|||||||
|
<template> |
||||||
|
<div class="embla"> |
||||||
|
<div class="embla__viewport" ref="emblaNode"> |
||||||
|
<div class="embla__container"> |
||||||
|
<div class="embla__slide" v-for="(slide, index) in slides" :key="index"> |
||||||
|
<img :src="slide" alt="" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 左右箭头 --> |
||||||
|
<button class="embla__button prev" @click="scrollPrev" :disabled="!canScrollPrev">‹</button> |
||||||
|
<button class="embla__button next" @click="scrollNext" :disabled="!canScrollNext">›</button> |
||||||
|
|
||||||
|
<!-- 圆点导航 --> |
||||||
|
<div class="embla__dots"> |
||||||
|
<button |
||||||
|
v-for="(_, index) in slides" |
||||||
|
:key="index" |
||||||
|
:class="{ 'is-selected': state.selectedIndex == index }" |
||||||
|
@click="scrollTo(index)" |
||||||
|
> |
||||||
|
{{}} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup> |
||||||
|
import { onMounted, ref, reactive } from 'vue' |
||||||
|
import EmblaCarousel from 'embla-carousel' |
||||||
|
|
||||||
|
const emblaNode = ref(null) |
||||||
|
const embla = ref(null) |
||||||
|
|
||||||
|
const state = reactive({ |
||||||
|
selectedIndex: 0, |
||||||
|
canScrollPrev: false, |
||||||
|
canScrollNext: false, |
||||||
|
}) |
||||||
|
|
||||||
|
const slides = ['/images/banner/bk1000.png', '/images/banner/bk2000.png'] |
||||||
|
|
||||||
|
const updateButtons = () => { |
||||||
|
if (!embla.value) return |
||||||
|
state.canScrollPrev = embla.value.canScrollPrev() |
||||||
|
state.canScrollNext = embla.value.canScrollNext() |
||||||
|
state.selectedIndex = embla.value.selectedScrollSnap() |
||||||
|
} |
||||||
|
|
||||||
|
const scrollPrev = () => embla.value?.scrollPrev() |
||||||
|
const scrollNext = () => embla.value?.scrollNext() |
||||||
|
const scrollTo = index => embla.value?.scrollTo(index) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
embla.value = EmblaCarousel(emblaNode.value, { loop: true }) |
||||||
|
|
||||||
|
const updateOnSelect = () => { |
||||||
|
state.selectedIndex = embla.value.selectedScrollSnap() |
||||||
|
state.canScrollPrev = embla.value.canScrollPrev() |
||||||
|
state.canScrollNext = embla.value.canScrollNext() |
||||||
|
} |
||||||
|
|
||||||
|
embla.value.on('select', updateOnSelect) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.embla { |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
.embla__viewport { |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.embla__container { |
||||||
|
display: flex; |
||||||
|
} |
||||||
|
.embla__slide { |
||||||
|
flex: 0 0 100%; |
||||||
|
padding: 10px; |
||||||
|
} |
||||||
|
.embla__button { |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
transform: translateY(-50%); |
||||||
|
background: #00000088; |
||||||
|
color: white; |
||||||
|
border: none; |
||||||
|
padding: 10px; |
||||||
|
cursor: pointer; |
||||||
|
z-index: 1; |
||||||
|
} |
||||||
|
.embla__button.prev { |
||||||
|
left: 10px; |
||||||
|
} |
||||||
|
.embla__button.next { |
||||||
|
right: 10px; |
||||||
|
} |
||||||
|
.embla__dots { |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
gap: 8px; |
||||||
|
margin-top: 10px; |
||||||
|
} |
||||||
|
.embla__dots button { |
||||||
|
width: 10px; |
||||||
|
height: 10px; |
||||||
|
border-radius: 50%; |
||||||
|
border: none; |
||||||
|
background: #ccc; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.embla__dots .is-selected { |
||||||
|
background: #333; |
||||||
|
} |
||||||
|
</style> |
@ -1,85 +0,0 @@ |
|||||||
<template> |
|
||||||
<ul |
|
||||||
class="grid grid-cols-1 grid-rows-none gap-4 overflow-auto xl:max-h-[56rem] xl:grid-rows-2 lg:gap-4 md:grid-cols-12 md:grid-rows-3" |
|
||||||
> |
|
||||||
<li |
|
||||||
v-for="item in gridItems" |
|
||||||
:key="item.title" |
|
||||||
:class="cn('min-h-[14rem] list-none', item.area)" |
|
||||||
> |
|
||||||
<div class="rounded-2.5xl relative h-full border p-2 md:rounded-3xl md:p-3"> |
|
||||||
<GlowingEffect |
|
||||||
:spread="40" |
|
||||||
:glow="true" |
|
||||||
:disabled="false" |
|
||||||
:proximity="64" |
|
||||||
:inactive-zone="0.01" |
|
||||||
/> |
|
||||||
<div |
|
||||||
class="border-0.75 relative flex h-full flex-col justify-between gap-6 overflow-hidden rounded-xl p-6 md:p-6 dark:shadow-[0px_0px_27px_0px_#2D2D2D]" |
|
||||||
> |
|
||||||
<div class="relative flex flex-1 flex-col justify-between gap-3"> |
|
||||||
<div class="w-fit rounded-lg border border-gray-600 p-2"> |
|
||||||
<Icon class="size-4 text-black dark:text-neutral-500" :name="item.icon"></Icon> |
|
||||||
</div> |
|
||||||
<div class="space-y-3"> |
|
||||||
<h3 |
|
||||||
class="-tracking-4 text-balance pt-0.5 font-sans text-xl/[1.375rem] font-semibold text-black md:text-2xl/[1.875rem] dark:text-white" |
|
||||||
> |
|
||||||
{{ item.title }} |
|
||||||
</h3> |
|
||||||
<h2 |
|
||||||
class="font-sans text-sm/[1.125rem] text-black md:text-base/[1.375rem] dark:text-neutral-400 [&_b]:md:font-semibold [&_strong]:md:font-semibold" |
|
||||||
> |
|
||||||
{{ item.description }} |
|
||||||
</h2> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</li> |
|
||||||
</ul> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script lang="ts" setup> |
|
||||||
import GlowingEffect from '@/components/ui/glowing-effect/GlowingEffect.vue' |
|
||||||
import { cn } from '@/lib/utils' |
|
||||||
|
|
||||||
const gridItems = [ |
|
||||||
{ |
|
||||||
area: 'md:[grid-area:1/1/2/7] xl:[grid-area:1/1/2/5]', |
|
||||||
icon: 'lucide:box', |
|
||||||
title: 'Unbox Endless Possibilities', |
|
||||||
description: |
|
||||||
'Open up Inspira UI to discover so many features, you’ll wonder if you’ve wandered into a magical subscription for infinite goodies.', |
|
||||||
}, |
|
||||||
{ |
|
||||||
area: 'md:[grid-area:1/7/2/13] xl:[grid-area:2/1/3/5]', |
|
||||||
icon: 'lucide:settings', |
|
||||||
title: 'Crank the Dials to Eleven', |
|
||||||
description: |
|
||||||
'We packed Inspira UI with enough customizable settings to keep you tweaking forever. If it’s broken, you probably forgot to flip one more switch!', |
|
||||||
}, |
|
||||||
{ |
|
||||||
area: 'md:[grid-area:2/1/3/7] lg:[grid-area:1/5/3/8]', |
|
||||||
icon: 'lucide:music', |
|
||||||
title: 'Dance Your Way to Better UI', |
|
||||||
description: |
|
||||||
'Forget dull interfaces—Inspira UI brings your Vue and Nuxt apps to life with animations so smooth, you’ll wonder!', |
|
||||||
}, |
|
||||||
{ |
|
||||||
area: 'md:[grid-area:2/7/3/13] xl:[grid-area:1/8/2/13]', |
|
||||||
icon: 'lucide:sparkles', |
|
||||||
title: 'Spark a Little Magic', |
|
||||||
description: |
|
||||||
'Make your interface shine brighter than your future. Inspira UI turns that dull design into an enchanting experience—fairy dust included!', |
|
||||||
}, |
|
||||||
{ |
|
||||||
area: 'md:[grid-area:3/1/4/13] xl:[grid-area:2/8/3/13]', |
|
||||||
icon: 'lucide:search', |
|
||||||
title: 'Seek and You Shall Find', |
|
||||||
description: |
|
||||||
'Our search is so advanced it might unearth your lost socks. Just don’t blame us when you realize they don’t match!', |
|
||||||
}, |
|
||||||
] |
|
||||||
</script> |
|
Before Width: | Height: | Size: 428 KiB After Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 428 KiB |
After Width: | Height: | Size: 2.2 MiB |
After Width: | Height: | Size: 685 KiB |
After Width: | Height: | Size: 335 KiB |
Loading…
Reference in new issue