replace el-carousel with Carousel component

This commit is contained in:
pa
2026-01-11 21:55:19 +09:00
committed by Natsumi
parent 5fa2d4d465
commit 6222becd3d
11 changed files with 1184 additions and 104 deletions

View File

@@ -354,45 +354,24 @@
:indeterminate="true"
:percentage="100"
:stroke-width="3"
style="margin: 10px 0; max-width: 240px" />
<el-carousel
v-if="avatarDialog.galleryImages.length"
type="card"
:autoplay="false"
height="200px">
<el-carousel-item v-for="imageUrl in avatarDialog.galleryImages" :key="imageUrl">
<img
:src="imageUrl"
style="width: 100%; height: 100%; object-fit: contain"
@click="showFullscreenImageDialog(imageUrl)"
loading="lazy" />
<div
v-if="avatarDialog.ref.authorId === currentUser.id"
style="position: absolute; bottom: 35px; left: 38%">
<el-button
size="small"
:icon="Back"
circle
class="x-link"
style="margin-left: 0"
@click.stop="reorderAvatarGalleryImage(imageUrl, -1)"></el-button>
<el-button
size="small"
:icon="Right"
circle
class="x-link"
style="margin-left: 0"
@click.stop="reorderAvatarGalleryImage(imageUrl, 1)"></el-button>
<el-button
size="small"
:icon="Delete"
circle
class="x-link"
style="margin-left: 0"
@click.stop="deleteAvatarGalleryImage(imageUrl)"></el-button>
</div>
</el-carousel-item>
</el-carousel>
style="margin: 10px 0" />
<div class="mt-2 w-[80%] ml-20">
<Carousel v-if="avatarDialog.galleryImages.length" class="w-full">
<CarouselContent class="h-50">
<CarouselItem v-for="imageUrl in avatarDialog.galleryImages" :key="imageUrl">
<div class="relative h-50 w-full">
<img
:src="imageUrl"
style="width: 100%; height: 100%; object-fit: contain"
@click="showFullscreenImageDialog(imageUrl)"
loading="lazy" />
</div>
</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
</div>
<div v-if="avatarDialog.ref.publishedListings?.length">
<span class="name">{{ t('dialog.avatar.info.listings') }}</span>
@@ -560,6 +539,7 @@
User,
Warning
} from '@element-plus/icons-vue';
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel';
import { CircleCheck, Ellipsis, RefreshCcw, Star, Trash2 } from 'lucide-vue-next';
import { computed, defineAsyncComponent, nextTick, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';

View File

@@ -0,0 +1,56 @@
<script setup>
import { cn } from '@/lib/utils';
import { useProvideCarousel } from './useCarousel';
const props = defineProps({
opts: { type: Object, required: false },
plugins: { type: null, required: false },
orientation: { type: String, required: false, default: 'horizontal' },
class: { type: null, required: false }
});
const emits = defineEmits(['init-api']);
const { canScrollNext, canScrollPrev, carouselApi, carouselRef, orientation, scrollNext, scrollPrev } =
useProvideCarousel(props, emits);
defineExpose({
canScrollNext,
canScrollPrev,
carouselApi,
carouselRef,
orientation,
scrollNext,
scrollPrev
});
function onKeyDown(event) {
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
if (event.key === prevKey) {
event.preventDefault();
scrollPrev();
return;
}
if (event.key === nextKey) {
event.preventDefault();
scrollNext();
}
}
</script>
<template>
<div
data-slot="carousel"
:class="cn('relative', props.class)"
role="region"
aria-roledescription="carousel"
tabindex="0"
@keydown="onKeyDown">
<slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev />
</div>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { cn } from '@/lib/utils';
import { useCarousel } from './useCarousel';
defineOptions({
inheritAttrs: false
});
const props = defineProps({
class: { type: null, required: false }
});
const { carouselRef, orientation } = useCarousel();
</script>
<template>
<div ref="carouselRef" data-slot="carousel-content" class="overflow-hidden">
<div
:class="cn('flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', props.class)"
v-bind="$attrs">
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { cn } from '@/lib/utils';
import { useCarousel } from './useCarousel';
const props = defineProps({
class: { type: null, required: false }
});
const { orientation } = useCarousel();
</script>
<template>
<div
data-slot="carousel-item"
role="group"
aria-roledescription="slide"
:class="cn('min-w-0 shrink-0 grow-0 basis-full', orientation === 'horizontal' ? 'pl-4' : 'pt-4', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,38 @@
<script setup>
import { ArrowRight } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useCarousel } from './useCarousel';
const props = defineProps({
variant: { type: String, required: false, default: 'outline' },
size: { type: String, required: false, default: 'icon' },
class: { type: null, required: false }
});
const { orientation, canScrollNext, scrollNext } = useCarousel();
</script>
<template>
<Button
data-slot="carousel-next"
:disabled="!canScrollNext"
:class="
cn(
'absolute size-8 rounded-full',
orientation === 'horizontal'
? 'top-1/2 -right-12 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
props.class
)
"
:variant="variant"
:size="size"
@click="scrollNext">
<slot>
<ArrowRight />
<span class="sr-only">Next Slide</span>
</slot>
</Button>
</template>

View File

@@ -0,0 +1,38 @@
<script setup>
import { ArrowLeft } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useCarousel } from './useCarousel';
const props = defineProps({
variant: { type: String, required: false, default: 'outline' },
size: { type: String, required: false, default: 'icon' },
class: { type: null, required: false }
});
const { orientation, canScrollPrev, scrollPrev } = useCarousel();
</script>
<template>
<Button
data-slot="carousel-previous"
:disabled="!canScrollPrev"
:class="
cn(
'absolute size-8 rounded-full',
orientation === 'horizontal'
? 'top-1/2 -left-12 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
props.class
)
"
:variant="variant"
:size="size"
@click="scrollPrev">
<slot>
<ArrowLeft />
<span class="sr-only">Previous Slide</span>
</slot>
</Button>
</template>

View File

@@ -0,0 +1,7 @@
export { default as Carousel } from './Carousel.vue';
export { default as CarouselContent } from './CarouselContent.vue';
export { default as CarouselItem } from './CarouselItem.vue';
export { default as CarouselNext } from './CarouselNext.vue';
export { default as CarouselPrevious } from './CarouselPrevious.vue';
export { useCarousel } from './useCarousel';

View File

@@ -0,0 +1,63 @@
import { onMounted, ref } from 'vue';
import { createInjectionState } from '@vueuse/core';
import emblaCarouselVue from 'embla-carousel-vue';
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
(props, emits) => {
const { opts, orientation, plugins } = props;
const [emblaNode, emblaApi] = emblaCarouselVue(
{
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y'
},
plugins
);
function scrollPrev() {
emblaApi.value?.scrollPrev();
}
function scrollNext() {
emblaApi.value?.scrollNext();
}
const canScrollNext = ref(false);
const canScrollPrev = ref(false);
function onSelect(api) {
canScrollNext.value = api?.canScrollNext() || false;
canScrollPrev.value = api?.canScrollPrev() || false;
}
onMounted(() => {
if (!emblaApi.value) return;
emblaApi.value?.on('init', onSelect);
emblaApi.value?.on('reInit', onSelect);
emblaApi.value?.on('select', onSelect);
emits('init-api', emblaApi.value);
});
return {
carouselRef: emblaNode,
carouselApi: emblaApi,
canScrollPrev,
canScrollNext,
scrollPrev,
scrollNext,
orientation
};
}
);
function useCarousel() {
const carouselState = useInjectCarousel();
if (!carouselState)
throw new Error('useCarousel must be used within a <Carousel />');
return carouselState;
}
export { useCarousel, useProvideCarousel };