feat(slider): add slider feature

This commit is contained in:
2025-11-01 17:32:28 +03:00
parent 0cccc7e3d7
commit 3049bd3101
37 changed files with 685 additions and 256 deletions

View File

@@ -1,100 +0,0 @@
<template>
<div v-if="slides.length > 0" class="app-banner px-4">
<Swiper
class="select-none"
:slides-per-view="1"
:space-between="50"
pagination
:pagination="{ clickable: true }"
@swiper="onSwiper"
@slideChange="onSlideChange"
>
<SwiperSlide v-for="slide in slides" :key="slide.id">
<RouterLink
v-if="slide?.link?.type === 'category'"
:to="{name: 'product.categories.show', params: {category_id: slide.link.value.category_id}}"
@click="sliderClick(slide)"
>
<img :src="slide.image" :alt="slide.title">
</RouterLink>
<RouterLink
v-else-if="slide?.link?.type === 'product'"
:to="{name: 'product.show', params: {id: slide.link.value.product_id}}"
@click="sliderClick(slide)"
>
<img :src="slide.image" :alt="slide.title">
</RouterLink>
<img
v-else-if="slide?.link?.type === 'url'"
:src="slide.image"
:alt="slide.title"
@click="openExternalLink(slide.link.value.url, slide)"
>
<img v-else :src="slide.image" :alt="slide.title"/>
</SwiperSlide>
</Swiper>
</div>
</template>
<script setup>
import {Swiper, SwiperSlide} from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/navigation';
import {onMounted, ref} from "vue";
import {fetchBanner} from "@/utils/ftch.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
const yaMetrika = useYaMetrikaStore();
const slides = ref([]);
const onSwiper = (swiper) => {
console.log(swiper);
};
const onSlideChange = () => {
console.log('slide change');
};
function sliderClick(slide) {
yaMetrika.reachGoal(YA_METRIKA_GOAL.SLIDER_HOME_CLICK, {
banner: slide.title,
});
}
function openExternalLink(link, slide) {
if (! link) {
return;
}
yaMetrika.reachGoal(YA_METRIKA_GOAL.SLIDER_HOME_CLICK, {
banner: slide.title,
});
window.Telegram.WebApp.openLink(link, {try_instant_view: false});
}
onMounted(async () => {
const response = await fetchBanner();
slides.value = response.data;
})
</script>
<style>
.app-banner .swiper-horizontal > .swiper-pagination-bullets {
position: relative;
bottom: 10px;
}
.app-banner .swiper-horizontal .swiper-slide {
display: flex;
align-items: center;
justify-content: center;
}
.app-banner .swiper-horizontal .swiper-slide img {
border-radius: var(--radius-box);
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div
v-if="sliders.mainpage_slider.is_enabled && sliders.mainpage_slider.slides.length > 0"
class="app-banner"
:class="classList"
>
<Swiper
:effect="slideEffect"
class="select-none"
:slides-per-view="1"
:space-between="sliders.mainpage_slider.space_between"
:pagination="pagination"
:lazy="true"
:modules="modules"
:scrollbar="scrollbar"
:free-mode="sliders.mainpage_slider.free_mode"
:loop="sliders.mainpage_slider.loop"
:autoplay="autoplay"
@swiper="onSwiper"
@slideChange="onSlideChange"
>
<SwiperSlide v-for="slide in sliders.mainpage_slider.slides" :key="slide.id">
<RouterLink
v-if="slide?.link?.type === 'category'"
:to="{name: 'product.categories.show', params: {category_id: slide.link.value.category_id}}"
@click="sliderClick(slide)"
>
<img :src="slide.image" :alt="slide.title" loading="lazy">
</RouterLink>
<RouterLink
v-else-if="slide?.link?.type === 'product'"
:to="{name: 'product.show', params: {id: slide.link.value.product_id}}"
@click="sliderClick(slide)"
>
<img :src="slide.image" :alt="slide.title" loading="lazy">
</RouterLink>
<img
v-else-if="slide?.link?.type === 'url'"
:src="slide.image"
:alt="slide.title"
loading="lazy"
@click="openExternalLink(slide.link.value.url, slide)"
>
<img v-else :src="slide.image" :alt="slide.title" loading="lazy"/>
</SwiperSlide>
</Swiper>
</div>
</template>
<script setup>
import {Swiper, SwiperSlide} from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/navigation';
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {EffectCoverflow, EffectCards, EffectCube, EffectFlip, Scrollbar, Autoplay} from 'swiper/modules';
import {computed, onMounted} from "vue";
import {useSlidersStore} from "@/stores/SlidersStore.js";
const sliders = useSlidersStore();
const yaMetrika = useYaMetrikaStore();
const modules = [
Autoplay,
EffectCards,
EffectFlip,
EffectCube,
Scrollbar,
EffectCoverflow,
];
const classList = computed(() => {
if (sliders.mainpage_slider.effect === 'cards') {
return ['px-8'];
}
if (sliders.mainpage_slider.effect === 'flip') {
return ['px-4', 'pb-4', 'pt-4'];
}
if (sliders.mainpage_slider.effect === 'cube') {
return ['px-4', 'pb-10'];
}
return ['px-4'];
});
const onSwiper = (swiper) => {
console.log(swiper);
};
const onSlideChange = () => {
console.log('slide change');
};
const slideEffect = computed(() => {
if (sliders.mainpage_slider.effect === 'slide') {
return null;
}
return sliders.mainpage_slider.effect;
});
const pagination = computed(() => {
if (sliders.mainpage_slider.pagination) {
return {
clickable: true, dynamicBullets: false,
};
}
return false;
});
const scrollbar = computed(() => {
if (sliders.mainpage_slider.scrollbar) {
return {
hide: true,
};
}
return false;
});
const autoplay = computed(() => {
if (sliders.mainpage_slider.autoplay) {
return {
delay: 3000,
reverseDirection: false,
};
}
return false;
});
function sliderClick(slide) {
yaMetrika.reachGoal(YA_METRIKA_GOAL.SLIDER_HOME_CLICK, {
banner: slide.title,
});
}
function openExternalLink(link, slide) {
if (!link) {
return;
}
yaMetrika.reachGoal(YA_METRIKA_GOAL.SLIDER_HOME_CLICK, {
banner: slide.title,
});
window.Telegram.WebApp.openLink(link, {try_instant_view: false});
}
onMounted(() => {
console.debug('[Mainpage Slider] Status: ', sliders.mainpage_slider);
});
</script>
<style>
.app-banner {
aspect-ratio: 740 / 400;
}
.app-banner .swiper {
overflow: visible;
}
.app-banner .swiper-horizontal > .swiper-pagination-bullets {
position: relative;
bottom: 10px;
}
.app-banner .swiper-horizontal .swiper-slide {
display: flex;
align-items: center;
justify-content: center;
}
.app-banner .swiper-horizontal .swiper-slide img {
border-radius: var(--radius-box);
}
</style>

View File

@@ -16,6 +16,7 @@ import 'swiper/element/bundle';
import 'swiper/css/bundle';
import AppLoading from "@/AppLoading.vue";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import {useSlidersStore} from "@/stores/SlidersStore.js";
register();
const pinia = createPinia();
@@ -26,6 +27,7 @@ app
.use(VueTelegramPlugin);
const settings = useSettingsStore();
useSlidersStore().fetchMainpageSlider();
const appLoading = createApp(AppLoading);
appLoading.mount('#app');

View File

@@ -0,0 +1,26 @@
import {defineStore} from "pinia";
import {fetchBanner} from "@/utils/ftch.js";
export const useSlidersStore = defineStore('sliders', {
state: () => ({
mainpage_slider: {
is_enabled: false,
space_between: 30,
autoplay: false,
effect: 'cube', // null, flip, cards, cube
pagination: false,
scrollbar: false,
free_mode: false,
loop: false,
slides: [],
},
}),
actions: {
async fetchMainpageSlider() {
console.debug('[Sliders Store] Fetch mainpage slider from server.');
const response = await fetchBanner();
this.mainpage_slider = Object.assign({}, this.mainpage_slider, response.data);
}
},
});

View File

@@ -6,6 +6,7 @@
html, body, #app {
width: 100%;
height: 100%;
overflow-x: hidden;
}
html {

View File

@@ -2,7 +2,9 @@
<div ref="goodsRef" class="pb-20">
<CategoriesInline/>
<Banner/>
<div class="overflow-hidden">
<MainpageSlider/>
</div>
<div class="px-5 fixed z-50 w-full opacity-90" style="bottom: calc(var(--tg-safe-area-inset-bottom, 0px) + 80px);">
<div class="flex justify-center">
@@ -31,12 +33,12 @@
<script setup>
import ProductsList from "@/components/ProductsList.vue";
import CategoriesInline from "../components/CategoriesInline.vue";
import {nextTick, onActivated, onMounted, ref, toRaw} from "vue";
import {onActivated, onMounted, ref, toRaw} from "vue";
import IconFunnel from "@/components/Icons/IconFunnel.vue";
import {useRouter} from "vue-router";
import ftch from "@/utils/ftch.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import Banner from "@/components/Banner.vue";
import MainpageSlider from "@/components/MainpageSlider.vue";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useSettingsStore} from "@/stores/SettingsStore.js";