fix: fix dock layout

This commit is contained in:
2025-12-21 18:06:04 +03:00
parent 28d80d0f19
commit bdbdfc3650
9 changed files with 150 additions and 91 deletions

View File

@@ -1,34 +1,31 @@
<template> <template>
<div class="drawer h-full"> <div class="app-container">
<input id="app-drawer" type="checkbox" class="drawer-toggle" v-model="drawerOpen"/> <header class="app-header bg-base-200 w-full"></header>
<div class="drawer-content"> <section class="app-content">
<div class="app-container"> <FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
<header class="app-header bg-base-200 w-full"></header>
<section class="telecart-main-section"> <div class="fixed inset-0 z-50 bg-white flex items-center justify-center text-center p-4
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
<div class="fixed inset-0 z-50 bg-white flex items-center justify-center text-center p-4
[@supports(color:oklch(0%_0_0))]:hidden"> [@supports(color:oklch(0%_0_0))]:hidden">
<BrowserNotSupported/> <BrowserNotSupported/>
</div> </div>
<AppDebugMessage v-if="settings.app_debug"/> <AppDebugMessage v-if="settings.app_debug"/>
<RouterView v-slot="{ Component, route }"> <RouterView v-slot="{ Component, route }">
<KeepAlive include="Home,Products" :key="filtersStore.paramsHashForRouter"> <KeepAlive include="Home,Products" :key="filtersStore.paramsHashForRouter">
<component :is="Component" :key="route.fullPath"/> <component :is="Component" :key="route.fullPath"/>
</KeepAlive> </KeepAlive>
</RouterView> </RouterView>
<PrivacyPolicy v-if="! settings.is_privacy_consented"/> <PrivacyPolicy v-if="! settings.is_privacy_consented"/>
<Dock v-if="isAppDockShown"/> <Dock v-if="isAppDockShown"/>
<div class="dock-spacer bg-base-100 z-50"></div>
<div <div
v-if="swiperBack.isActive.value" v-if="swiperBack.isActive.value"
class="fixed top-1/2 left-0 z-50 class="fixed top-1/2 left-0 z-50
w-20 w-20
h-20 h-20
-translate-y-1/2 -translate-y-1/2
@@ -39,29 +36,18 @@
py-2 py-2
text-primary-content text-primary-content
" "
:class="{ :class="{
'bg-primary': swiperBack.deltaX.value < swiperBack.ACTIVATION_THRESHOLD, 'bg-primary': swiperBack.deltaX.value < swiperBack.ACTIVATION_THRESHOLD,
'bg-accent': swiperBack.deltaX.value >= swiperBack.ACTIVATION_THRESHOLD, 'bg-accent': swiperBack.deltaX.value >= swiperBack.ACTIVATION_THRESHOLD,
}" }"
:style="{ transform: `translate(${easeOut(swiperBack.deltaX.value/10, 30)}px, -50%)` }" :style="{ transform: `translate(${easeOut(swiperBack.deltaX.value/10, 30)}px, -50%)` }"
> >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" /> <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg> </svg>
</div>
</section>
</div> </div>
</div> </section>
<div class="drawer-side z-50 safe-top">
<label for="app-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
<li><a href="#">🏠 Главная</a></li>
<li><a href="#">🛒 Корзина</a></li>
<li><a @click="drawerOpen = false"> Закрыть</a></li>
</ul>
</div>
</div> </div>
</template> </template>
@@ -72,12 +58,11 @@ import {useRoute, useRouter} from "vue-router";
import {useSettingsStore} from "@/stores/SettingsStore.js"; import {useSettingsStore} from "@/stores/SettingsStore.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import {useKeyboardStore} from "@/stores/KeyboardStore.js"; import {useKeyboardStore} from "@/stores/KeyboardStore.js";
import CartButton from "@/components/CartButton.vue";
import Dock from "@/components/Dock.vue"; import Dock from "@/components/Dock.vue";
import AppDebugMessage from "@/components/AppDebugMessage.vue"; import AppDebugMessage from "@/components/AppDebugMessage.vue";
import PrivacyPolicy from "@/components/PrivacyPolicy.vue"; import PrivacyPolicy from "@/components/PrivacyPolicy.vue";
import {useSwipeBack} from "@/composables/useSwipeBack.js";
import BrowserNotSupported from "@/BrowserNotSupported.vue"; import BrowserNotSupported from "@/BrowserNotSupported.vue";
import {useSwipeBack} from "@/composables/useSwipeBack.js";
const tg = useMiniApp(); const tg = useMiniApp();
const platform = ref(); const platform = ref();
@@ -93,9 +78,6 @@ const filtersStore = useProductFiltersStore();
const keyboardStore = useKeyboardStore(); const keyboardStore = useKeyboardStore();
const backButton = window.Telegram.WebApp.BackButton; const backButton = window.Telegram.WebApp.BackButton;
const haptic = window.Telegram.WebApp.HapticFeedback; const haptic = window.Telegram.WebApp.HapticFeedback;
const drawerOpen = ref(false);
// Инициализация жеста Swipe Back (без визуального индикатора)
const swiperBack = useSwipeBack(); const swiperBack = useSwipeBack();
const routesToHideAppDock = [ const routesToHideAppDock = [
@@ -127,10 +109,6 @@ function navigateBack() {
router.back(); router.back();
} }
function toggleDrawer() {
drawerOpen.value = !drawerOpen.value;
}
watch( watch(
() => route.name, () => route.name,
() => { () => {
@@ -151,6 +129,16 @@ function handleClickOutside(e) {
} }
} }
watch(
() => route.name,
(name) => {
let height = '72px'; // дефолт
if (name === 'product.show') height = '146px';
document.documentElement.style.setProperty('--dock-height', height);
},
{ immediate: true }
);
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleClickOutside); document.removeEventListener('click', handleClickOutside);
}); });
@@ -159,3 +147,19 @@ onMounted(() => {
document.addEventListener('click', handleClickOutside); document.addEventListener('click', handleClickOutside);
}); });
</script> </script>
<style scoped>
.dock-spacer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: calc(
var(--tg-content-safe-area-inset-bottom, 0px)
+ var(--tg-safe-area-inset-bottom, 0px)
);
pointer-events: none;
}
</style>

View File

@@ -77,3 +77,25 @@ function onDockItemClick() {
haptic.selectionChanged(); haptic.selectionChanged();
} }
</script> </script>
<style scoped>
.dock {
position: fixed;
left: 0;
right: 0;
height: var(--dock-height);
bottom: calc(
var(--tg-content-safe-area-inset-bottom, 0px)
+ var(--tg-safe-area-inset-bottom, 0px)
);
padding-bottom: 0;
display: flex;
align-items: center;
justify-content: space-around;
background: var(--color-base-100);
z-index: 50;
}
</style>

View File

@@ -17,9 +17,9 @@
<SwiperSlide <SwiperSlide
v-for="product in block.data.products.data" v-for="product in block.data.products.data"
:key="product.id" :key="product.id"
class="radius-box bg-base-100 shadow-sm p-2" class="pb-1"
> >
<div> <div class="radius-box bg-base-100 shadow-sm p-2">
<RouterLink <RouterLink
:to="{name: 'product.show', params: {id: product.id}}" :to="{name: 'product.show', params: {id: product.id}}"
@click="slideClick(product)" @click="slideClick(product)"
@@ -30,7 +30,6 @@
</div> </div>
</RouterLink> </RouterLink>
</div> </div>
</SwiperSlide> </SwiperSlide>
</Swiper> </Swiper>
</BaseBlock> </BaseBlock>

View File

@@ -0,0 +1,10 @@
<template>
</template>
<script setup>
import {useSwipeBack} from "@/composables/useSwipeBack.js";
</script>

View File

@@ -1,4 +1,4 @@
import {createApp} from 'vue'; import {createApp, ref} from 'vue';
import App from './App.vue'; import App from './App.vue';
import './style.css'; import './style.css';
import {VueTelegramPlugin} from 'vue-tg'; import {VueTelegramPlugin} from 'vue-tg';
@@ -118,8 +118,16 @@ settings.load()
app.mount('#app'); app.mount('#app');
}) })
.then(() => window.Telegram.WebApp.ready()) .then(() => window.Telegram.WebApp.ready())
.then(() => {
const con = console;
window.Telegram.WebApp.onEvent('viewportChanged', (state) => {
con.log('[Init]: viewportChanged', state.isStateStable, this.viewportHeight);
});
})
.catch(error => { .catch(error => {
const code = error.code ?? error.response._data.code; console.error(error);
const code = error.code ?? error?.response?._data.code;
let ErrorComponent; let ErrorComponent;
switch (code) { switch (code) {

View File

@@ -64,3 +64,4 @@ router.beforeEach((to, from, next) => {
ym.prevPath = from.path; ym.prevPath = from.path;
next(); next();
}); });

View File

@@ -1,4 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@plugin "daisyui" { @plugin "daisyui" {
@@ -11,6 +12,7 @@
html, body, #app { html, body, #app {
overflow-x: hidden; overflow-x: hidden;
min-height: 100vh;
} }
:root { :root {
@@ -20,24 +22,37 @@ html, body, #app {
--swiper-pagination-bottom: 0px; --swiper-pagination-bottom: 0px;
--product_list_title_max_lines: 2; --product_list_title_max_lines: 2;
--tc-navbar-min-height: 3rem; --tc-navbar-min-height: 3rem;
} --dock-height: 72px;
#app {
position: relative;
/*padding-top: var(--tg-content-safe-area-inset-top);*/
padding-bottom: var(--tg-content-safe-area-inset-bottom);
padding-left: var(--tg-content-safe-area-inset-left);
padding-right: var(--tg-content-safe-area-inset-right);
} }
.app-container { .app-container {
/*padding-top: calc(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top));*/ min-height: 100vh;
padding-bottom: calc( display: flex;
var(--tg-safe-area-inset-bottom, 0px) flex-direction: column;
+ 72px
padding-left: calc(
var(--tg-content-safe-area-inset-left, 0px)
+ var(--tg-safe-area-inset-left, 0px)
);
padding-right: calc(
var(--tg-content-safe-area-inset-right, 0px)
+ var(--tg-safe-area-inset-right, 0px)
);
}
.app-content {
flex: 1 1 auto;
padding-top: calc(
var(--tg-content-safe-area-inset-top, 0px)
+ var(--tg-safe-area-inset-top, 0px)
);
padding-bottom: calc(
var(--dock-height)
+ var(--tg-content-safe-area-inset-bottom, 0px)
+ var(--tg-safe-area-inset-bottom, 0px)
); );
padding-left: var(--tg-safe-area-inset-left, 0px);
padding-right: var(--tg-safe-area-inset-right, 0px);
} }
.safe-top { .safe-top {
@@ -57,19 +72,17 @@ html, body, #app {
color: white; color: white;
} }
.telecart-main-section {
padding-top: calc(
var(--tg-content-safe-area-inset-top, 0rem)
+ var(--tg-safe-area-inset-top, 0rem)
/*+ var(--tc-navbar-min-height)*/
/*+ 1rem*/
);
}
html { html {
background-color: var(--color-base-200); background-color: var(--color-base-200);
} }
.radius-box { .radius-box {
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
} }
.bottom-fix {
bottom: calc(
var(--tg-content-safe-area-inset-bottom, 0px)
+ var(--tg-safe-area-inset-bottom, 0px)
);
}

View File

@@ -30,7 +30,7 @@
</div> </div>
<div <div
class="fixed px-4 pb-4 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex flex-col justify-between items-center gap-2 border-t-1 border-t-base-300"> class="fixed bottom-fix px-4 pb-4 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex flex-col justify-between items-center gap-2 border-t-1 border-t-base-300">
<div class="text-error">{{ checkout.errorMessage }}</div> <div class="text-error">{{ checkout.errorMessage }}</div>
<div> <div>

View File

@@ -6,12 +6,14 @@
</div> </div>
<div v-else-if="imagesLoaded && images.length === 0"> <div v-else-if="imagesLoaded && images.length === 0">
<div class="bg-base-200 aspect-square flex items-center justify-center flex-col text-base-300"> <div class="bg-base-200 aspect-square flex items-center justify-center flex-col text-base-300">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-18"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-18">
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z" clip-rule="evenodd" /> <path fill-rule="evenodd"
</svg> d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z"
<span class="text-xl">Нет изображений</span> clip-rule="evenodd"/>
</div> </svg>
<span class="text-xl">Нет изображений</span>
</div>
</div> </div>
<SingleProductImageSwiper v-else :images="images"/> <SingleProductImageSwiper v-else :images="images"/>
@@ -119,8 +121,8 @@
</div> </div>
<div v-if="product.product_id" <div v-if="product.product_id"
class="fixed px-4 pb-4 pt-4 bottom-0 left-0 w-full bg-base-100/95 backdrop-blur-md z-50 flex flex-col gap-3 border-t border-base-300 shadow-lg" class="fixed bottom-fix px-4 pb-4 pt-4 left-0 w-full bg-base-100/95 backdrop-blur-md z-50 flex flex-col gap-3 border-t border-base-300 shadow-lg"
style="padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));"> >
<template v-if="settings.store_enabled"> <template v-if="settings.store_enabled">
<div v-if="error" class="alert alert-error alert-sm py-2"> <div v-if="error" class="alert alert-error alert-sm py-2">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-4 w-4" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-4 w-4" fill="none"
@@ -306,15 +308,15 @@ function setQuantity(newQuantity) {
onMounted(async () => { onMounted(async () => {
// Явно сбрасываем скролл наверх при открытии страницы товара // Явно сбрасываем скролл наверх при открытии страницы товара
window.scrollTo({ top: 0, behavior: 'instant' }); window.scrollTo({top: 0, behavior: 'instant'});
isLoading.value = true; isLoading.value = true;
imagesLoaded.value = false; imagesLoaded.value = false;
// Запускаем оба запроса параллельно // Запускаем оба запроса параллельно
const productPromise = fetchProductById(productId.value); const productPromise = fetchProductById(productId.value);
const imagesPromise = fetchProductImages(productId.value); const imagesPromise = fetchProductImages(productId.value);
try { try {
// Ждем только загрузку продукта для рендеринга страницы // Ждем только загрузку продукта для рендеринга страницы
const response = await productPromise; const response = await productPromise;