Files
interview-demo-code/frontend/spa/src/App.vue
2025-12-21 18:06:04 +03:00

166 lines
4.6 KiB
Vue

<template>
<div class="app-container">
<header class="app-header bg-base-200 w-full"></header>
<section class="app-content">
<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">
<BrowserNotSupported/>
</div>
<AppDebugMessage v-if="settings.app_debug"/>
<RouterView v-slot="{ Component, route }">
<KeepAlive include="Home,Products" :key="filtersStore.paramsHashForRouter">
<component :is="Component" :key="route.fullPath"/>
</KeepAlive>
</RouterView>
<PrivacyPolicy v-if="! settings.is_privacy_consented"/>
<Dock v-if="isAppDockShown"/>
<div class="dock-spacer bg-base-100 z-50"></div>
<div
v-if="swiperBack.isActive.value"
class="fixed top-1/2 left-0 z-50
w-20
h-20
-translate-y-1/2
-translate-x-18
flex items-center justify-end
shadow-xl
rounded-full
py-2
text-primary-content
"
:class="{
'bg-primary': 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%)` }"
>
<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" />
</svg>
</div>
</section>
</div>
</template>
<script setup>
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
import {FullscreenViewport, useMiniApp, useWebAppViewport} from 'vue-tg';
import {useRoute, useRouter} from "vue-router";
import {useSettingsStore} from "@/stores/SettingsStore.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import {useKeyboardStore} from "@/stores/KeyboardStore.js";
import Dock from "@/components/Dock.vue";
import AppDebugMessage from "@/components/AppDebugMessage.vue";
import PrivacyPolicy from "@/components/PrivacyPolicy.vue";
import BrowserNotSupported from "@/BrowserNotSupported.vue";
import {useSwipeBack} from "@/composables/useSwipeBack.js";
const tg = useMiniApp();
const platform = ref();
platform.value = tg.platform;
const {disableVerticalSwipes} = useWebAppViewport();
disableVerticalSwipes();
const router = useRouter();
const route = useRoute();
const settings = useSettingsStore();
const filtersStore = useProductFiltersStore();
const keyboardStore = useKeyboardStore();
const backButton = window.Telegram.WebApp.BackButton;
const haptic = window.Telegram.WebApp.HapticFeedback;
const swiperBack = useSwipeBack();
const routesToHideAppDock = [
'product.show',
'checkout',
'order_created',
'filters',
];
function easeOut(value, max) {
const x = Math.min(Math.abs(value) / max, 1)
const eased = 1 - (1 - x) ** 3
return Math.sign(value) * eased * max
}
const isAppDockShown = computed(() => {
if (routesToHideAppDock.indexOf(route.name) === -1) {
// Скрываем Dock, если клавиатура открыта на странице поиска
if (route.name === 'search' && keyboardStore.isOpen) {
return false;
}
return true;
}
return false;
});
function navigateBack() {
haptic.impactOccurred('light');
router.back();
}
watch(
() => route.name,
() => {
if (route.name === 'home') {
backButton.hide();
backButton.offClick(navigateBack);
} else {
backButton.show();
backButton.onClick(navigateBack);
}
},
{immediate: true}
);
function handleClickOutside(e) {
if (!e.target.closest('input,select,textarea')) {
document.activeElement?.blur();
}
}
watch(
() => route.name,
(name) => {
let height = '72px'; // дефолт
if (name === 'product.show') height = '146px';
document.documentElement.style.setProperty('--dock-height', height);
},
{ immediate: true }
);
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
</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>