feat: добавлен жест swipe back для навигации назад

- Добавлен композабл useSwipeBack для обработки жеста свайпа от левого края
- Жест активируется при движении от левого края экрана (в пределах 20px)
- Навигация назад выполняется при достижении порога 40px
- Добавлена тактильная обратная связь при достижении порога
- Работает только не на главной странице, как и кнопка назад
- Игнорирует интерактивные элементы (input, button, swiper и т.д.)
This commit is contained in:
2025-12-01 22:55:29 +03:00
parent db8d1360fc
commit 17972993ca
2 changed files with 212 additions and 0 deletions

View File

@@ -47,6 +47,7 @@ import CartButton from "@/components/CartButton.vue";
import Dock from "@/components/Dock.vue";
import AppDebugMessage from "@/components/AppDebugMessage.vue";
import PrivacyPolicy from "@/components/PrivacyPolicy.vue";
import {useSwipeBack} from "@/composables/useSwipeBack.js";
const tg = useMiniApp();
const platform = ref();
@@ -64,6 +65,9 @@ const backButton = window.Telegram.WebApp.BackButton;
const haptic = window.Telegram.WebApp.HapticFeedback;
const drawerOpen = ref(false);
// Инициализация жеста Swipe Back (без визуального индикатора)
useSwipeBack();
const routesToHideAppDock = [
'product.show',
'checkout',

View File

@@ -0,0 +1,208 @@
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useHapticFeedback } from './useHapticFeedback.js';
/**
* Композабл для обработки жеста Swipe Back
* Реализует поведение, аналогичное нативному в Telegram MiniApp
* Без визуального индикатора
*/
export function useSwipeBack() {
const router = useRouter();
const route = useRoute();
const haptic = useHapticFeedback();
// Состояние жеста
const isActive = ref(false);
const deltaX = ref(0);
const progress = ref(0);
const hasTriggeredHaptic = ref(false);
// Конфигурация
const EDGE_THRESHOLD = 20; // Расстояние от левого края для активации (px)
const ACTIVATION_THRESHOLD = 40; // Пороговое расстояние для активации (px)
const MAX_DELTA = 80; // Максимальное расстояние для расчета прогресса (px)
let touchStartX = 0;
let touchStartY = 0;
let touchStartTime = 0;
let isTracking = false;
/**
* Вычисляет прогресс жеста (0-1)
*/
const calculateProgress = (currentDeltaX) => {
return Math.min(currentDeltaX / MAX_DELTA, 1);
};
/**
* Проверяет, можно ли использовать жест swipe back
*/
const canUseSwipeBack = computed(() => {
return route.name !== 'home';
});
/**
* Проверяет, начинается ли жест от левого края экрана
*/
const isStartingFromLeftEdge = (x) => {
return x <= EDGE_THRESHOLD;
};
/**
* Обработчик начала касания
*/
const handleTouchStart = (e) => {
// Проверяем, можно ли использовать жест (не на главной странице)
if (!canUseSwipeBack.value) return;
const touch = e.touches[0];
if (!touch) return;
// Проверяем, не является ли цель касания интерактивным элементом
const target = e.target;
if (target.closest('input, textarea, select, button, a, [role="button"], .swiper, .swiper-wrapper')) {
return;
}
touchStartX = touch.clientX;
touchStartY = touch.clientY;
touchStartTime = Date.now();
// Проверяем, начинается ли жест от левого края
if (isStartingFromLeftEdge(touchStartX)) {
isTracking = true;
isActive.value = true;
deltaX.value = 0;
progress.value = 0;
hasTriggeredHaptic.value = false;
}
};
/**
* Обработчик движения пальца
*/
const handleTouchMove = (e) => {
if (!isTracking) return;
const touch = e.touches[0];
if (!touch) return;
const currentX = touch.clientX;
const currentY = touch.clientY;
const currentDeltaX = currentX - touchStartX;
const currentDeltaY = Math.abs(currentY - touchStartY);
// Проверяем, что движение преимущественно горизонтальное
if (Math.abs(currentDeltaX) < Math.abs(currentDeltaY) * 0.5 && Math.abs(currentDeltaX) > 5) {
// Вертикальное движение - отменяем жест
resetGesture();
return;
}
// Если движение влево (отрицательное), уменьшаем прогресс
if (currentDeltaX < 0) {
deltaX.value = Math.max(currentDeltaX, -EDGE_THRESHOLD);
// Если вернулись слишком далеко влево, отменяем жест
if (currentDeltaX < -10) {
resetGesture();
return;
}
} else {
// Прямое обновление deltaX для положительного движения
deltaX.value = currentDeltaX;
}
// Обновляем прогресс только для положительного движения
progress.value = calculateProgress(Math.max(0, deltaX.value));
// Проверяем достижение порога активации
if (deltaX.value >= ACTIVATION_THRESHOLD && !hasTriggeredHaptic.value) {
// Срабатывает тактильная обратная связь один раз
if (haptic) {
haptic.impactOccurred('light');
}
hasTriggeredHaptic.value = true;
}
// Предотвращаем прокрутку страницы во время жеста
if (deltaX.value > 0) {
e.preventDefault();
}
};
/**
* Обработчик окончания касания
*/
const handleTouchEnd = () => {
if (!isTracking) return;
// Если жест достиг порога активации - выполняем навигацию назад
if (deltaX.value >= ACTIVATION_THRESHOLD) {
// Небольшая задержка для плавности
setTimeout(() => {
router.back();
resetGesture();
}, 50);
} else {
// Откат жеста без действия
resetGesture();
}
};
/**
* Обработчик отмены касания
*/
const handleTouchCancel = () => {
resetGesture();
};
/**
* Сброс состояния жеста
*/
const resetGesture = () => {
isTracking = false;
isActive.value = false;
deltaX.value = 0;
progress.value = 0;
hasTriggeredHaptic.value = false;
};
/**
* Инициализация обработчиков событий
*/
const init = () => {
document.addEventListener('touchstart', handleTouchStart, { passive: true });
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd, { passive: true });
document.addEventListener('touchcancel', handleTouchCancel, { passive: true });
};
/**
* Очистка обработчиков событий
*/
const cleanup = () => {
document.removeEventListener('touchstart', handleTouchStart);
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', handleTouchEnd);
document.removeEventListener('touchcancel', handleTouchCancel);
resetGesture();
};
onMounted(() => {
init();
});
onUnmounted(() => {
cleanup();
});
return {
isActive,
deltaX,
progress,
ACTIVATION_THRESHOLD,
MAX_DELTA,
};
}