feat: improve mainpage ui/ux

This commit is contained in:
2025-11-16 01:38:57 +03:00
parent f0837e5c94
commit f5d9d417b3
26 changed files with 222 additions and 184 deletions

View File

@@ -2,7 +2,7 @@
<a
href="#"
:key="category.id"
class="py-2 px-4 flex items-center mb-3"
class="flex items-center"
@click.prevent="$emit('onSelect', category)"
>
<div class="avatar">
@@ -11,7 +11,7 @@
</div>
</div>
<h3 class="ml-5 text-lg line-clamp-2">{{ category.name }}</h3>
<h3 class="ml-4 text-lg line-clamp-2">{{ category.name }}</h3>
</a>
</template>

View File

@@ -10,7 +10,7 @@
<RouterLink
:to="{name: 'product.categories.show',
params: { category_id: block.data.category_id }}"
class="btn btn-outline btn-xs"
class="btn btn-ghost btn-xs"
>
{{ block.data.all_text || 'Смотреть всё' }}
</RouterLink>
@@ -24,6 +24,7 @@
:autoplay="block.data?.carousel?.autoplay || false"
:freeMode="freeModeSettings"
:lazy="true"
@sliderMove="hapticScroll"
>
<SwiperSlide v-for="product in block.data.products.data" :key="product.id">
<RouterLink
@@ -31,14 +32,14 @@
@click="slideClick(product)"
>
<div class="text-center">
<img :src="product.images[0].url" :alt="product.name" loading="lazy">
<h3 class="product-title mt-4 text-sm">{{ product.name }}</h3>
<img :src="product.images[0].url" :alt="product.name" loading="lazy" class="product-image"/>
<ProductTitle :title="product.name"/>
<div v-if="product.special" class="mt-1">
<p class="text-xs line-through mr-2">{{ product.price }}</p>
<p class="text-lg font-medium">{{ product.special }}</p>
<span class="text-xs line-through mr-2">{{ product.price }}</span>
<span class="text-base font-medium">{{ product.special }}</span>
</div>
<p v-else class="mt-1 text-lg font-medium">{{ product.price }}</p>
<p v-else class="font-medium">{{ product.price }}</p>
</div>
</RouterLink>
</SwiperSlide>
@@ -50,13 +51,12 @@
<script setup>
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {Swiper, SwiperSlide} from "swiper/vue";
import ProductTitle from "@/components/ProductItem/ProductTitle.vue";
import {useHapticScroll} from "@/composables/useHapticScroll.js";
const hapticScroll = useHapticScroll(20, 'selectionChanged');
const yaMetrika = useYaMetrikaStore();
const freeModeSettings = {
enabled: false,
};
const props = defineProps({
block: {
type: Object,
@@ -64,6 +64,10 @@ const props = defineProps({
}
});
const freeModeSettings = {
enabled: props.block.data?.carousel?.freemode?.enabled || false,
};
function slideClick(product) {
if (props.block.goal_name) {
yaMetrika.reachGoal(props.block.goal_name, {
@@ -73,3 +77,9 @@ function slideClick(product) {
}
}
</script>
<style scoped>
.product-image {
border-radius: var(--radius-box);
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<section>
<header>
<div v-if="block.title" class="font-bold uppercase text-center">{{ block.title }}</div>
<div v-if="block.description" class="text-sm text-center">{{ block.description }}</div>
<section class="px-4">
<header class="mb-2">
<div v-if="block.title" class="font-bold uppercase">{{ block.title }}</div>
<div v-if="block.description" class="text-sm">{{ block.description }}</div>
</header>
<main>
<ProductsList

View File

@@ -2,7 +2,6 @@
<div
v-if="blocks.blocks?.length > 0"
v-for="(block, index) in blocks.blocks"
class="mb-5"
>
<template v-if="blockTypeToComponentMap[block.type]">
<component

View File

@@ -1,5 +1,5 @@
<template>
<div class="telecart-navbar fixed navbar bg-primary text-primary-content z-50 shadow-md" :class="{'pb-0' : platform !== 'ios'}">
<div class="navbar bg-neutral text-neutral-content">
<div class="navbar-start">
<div v-if="false" class="dropdown">
<button class="btn btn-ghost btn-circle" @click="toggleDrawer">
@@ -9,10 +9,10 @@
</div>
<div class="navbar-center">
<RouterLink :to="{name: 'home'}" class="text-xl flex items-center">
<div class="avatar mr-2">
<div v-if="settings.app_icon" class="h-8 rounded-full bg-base-100">
<img :src="settings.app_icon" class="h-8" alt=""/>
<RouterLink :to="{name: 'home'}" class="font-medium text-xl flex items-center">
<div class="mr-2">
<div v-if="settings.app_icon" class="max-h-10">
<img :src="settings.app_icon" class="max-h-10" :alt="settings.app_name"/>
</div>
</div>
@@ -47,10 +47,3 @@ function toggleDrawer() {
emits('drawer');
}
</script>
<style scoped>
.telecart-navbar {
padding-top: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));
min-height: var(--tc-navbar-min-height);
}
</style>

View File

@@ -1,24 +1,28 @@
<template>
<swiper-container ref="swiperEl" init="false" pagination-dynamic-bullets="true">
<swiper-slide
<Swiper
:lazy="true"
:modules="modules"
:pagination="pagination"
@sliderMove="hapticScroll"
class="radius-box"
>
<SwiperSlide
v-for="image in images"
:key="image.url"
class="bg-base-100 overflow-hidden"
style="aspect-ratio:1/1; border-radius:12px;"
>
<img
:src="image.url"
:alt="image.alt"
loading="lazy"
class="w-full h-full"
style="object-fit: contain"
class="w-full h-full radius-box"
/>
</swiper-slide>
</swiper-container>
</SwiperSlide>
</Swiper>
</template>
<script setup>
import {onActivated, onMounted, onUnmounted, ref} from "vue";
import {Swiper, SwiperSlide} from 'swiper/vue';
import {useHapticScroll} from "@/composables/useHapticScroll.js";
const props = defineProps({
images: {
@@ -27,61 +31,16 @@ const props = defineProps({
},
});
const params = {
injectStyles: [`
.swiper-pagination {
position: relative;
padding-top: 15px;
}
`],
pagination: {
clickable: true,
},
}
const swiperEl = ref(null);
onUnmounted(() => {
});
onMounted(() => {
const el = swiperEl.value;
if (!el) return;
el.addEventListener('swiperactiveindexchange', () => {
window.Telegram?.WebApp?.HapticFeedback?.selectionChanged();
});
Object.assign(el, params);
el.initialize();
// 👇 важно, особенно если картинки подгружаются не сразу
el.addEventListener('swiperinit', () => {
el.swiper.update();
});
});
onActivated(() => {
const el = swiperEl.value
if (!el) return;
// Если swiper есть, но pagination потерялся — уничтожаем
if (el.swiper) {
try {
el.swiper.destroy(true, true)
} catch (e) {
console.warn('Failed to destroy swiper', e)
}
}
// Переинициализация с параметрами
Object.assign(el, params)
el.initialize()
// Пересчёт пагинации после инициализации
el.addEventListener('swiperinit', () => {
el.swiper.update()
})
})
const modules = [];
const hapticScroll = useHapticScroll();
const pagination = {
clickable: true,
dynamicBullets: false,
};
</script>
<style scoped>
.radius-box {
border-radius: var(--radius-box);
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<h3 class="product-title mt-4 text-sm">{{ title }}</h3>
</template>
<script setup>
const props = defineProps({
title: {
type: String,
default: null,
}
});
</script>
<style scoped>
.product-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--product_list_title_max_lines, 3);
overflow: hidden;
}
</style>

View File

@@ -1,11 +1,11 @@
<template>
<div>
<div class="mx-auto max-w-2xl px-4 py-4 pb-14">
<div>
<h2 v-if="categoryName" class="text-lg font-bold mb-5 text-center">{{ categoryName }}</h2>
<template v-if="products.length > 0">
<div
class="products-grid grid grid-cols-2 gap-x-5 gap-y-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8"
class="products-grid grid grid-cols-2 gap-x-4 gap-y-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8"
>
<RouterLink
v-for="(product, index) in products"
@@ -15,7 +15,7 @@
@click="productClick(product, index)"
>
<ProductImageSwiper :images="product.images"/>
<h3 class="product-title mt-4 text-sm">{{ product.name }}</h3>
<ProductTitle :title="product.name"/>
<div v-if="product.special" class="mt-1">
<p class="text-xs line-through mr-2">{{ product.price }}</p>
@@ -48,7 +48,7 @@
<NoProducts v-else/>
</div>
<div class="fixed z-50 w-full opacity-90" style="bottom: calc(var(--tg-safe-area-inset-bottom, 0px) + 80px);">
<div v-if="false" class="fixed z-50 w-full opacity-90" style="bottom: calc(var(--tg-safe-area-inset-bottom, 0px) + 80px);">
<div class="flex justify-center">
<button
@click="showFilters"
@@ -74,6 +74,7 @@ import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import IconFunnel from "@/components/Icons/IconFunnel.vue";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import {useRouter} from "vue-router";
import ProductTitle from "@/components/ProductItem/ProductTitle.vue";
const router = useRouter();
const haptic = window.Telegram.WebApp.HapticFeedback;
@@ -156,12 +157,3 @@ function showFilters() {
router.push({name: 'filters'});
}
</script>
<style scoped>
.product-grid-card .product-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--product_list_title_max_lines, 3);
overflow: hidden;
}
</style>

View File

@@ -14,6 +14,7 @@
:autoplay="autoplay"
@swiper="onSwiper"
@slideChange="onSlideChange"
@sliderMove="hapticScroll"
>
<SwiperSlide v-for="slide in config.slides" :key="slide.id">
<RouterLink
@@ -58,12 +59,11 @@
<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 {Autoplay, EffectCards, EffectCoverflow, EffectCube, EffectFlip, Scrollbar} from 'swiper/modules';
import {computed, onMounted} from "vue";
import {useHapticScroll} from "@/composables/useHapticScroll.js";
const props = defineProps({
config: {
@@ -77,6 +77,7 @@ const props = defineProps({
}
});
const hapticScroll = useHapticScroll(20, 'impactOccurred', 'soft');
const yaMetrika = useYaMetrikaStore();
const modules = [
Autoplay,
@@ -182,8 +183,7 @@ onMounted(() => {
}
.app-banner .swiper-horizontal > .swiper-pagination-bullets {
position: relative;
bottom: 10px;
bottom: -20px;
}
.app-banner .swiper-horizontal .swiper-slide {