feat: добавлен жест swipe back для навигации назад
- Добавлен композабл useSwipeBack для обработки жеста свайпа от левого края - Жест активируется при движении от левого края экрана (в пределах 20px) - Навигация назад выполняется при достижении порога 40px - Добавлена тактильная обратная связь при достижении порога - Работает только не на главной странице, как и кнопка назад - Игнорирует интерактивные элементы (input, button, swiper и т.д.)
This commit is contained in:
@@ -47,6 +47,7 @@ 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";
|
||||||
|
|
||||||
const tg = useMiniApp();
|
const tg = useMiniApp();
|
||||||
const platform = ref();
|
const platform = ref();
|
||||||
@@ -64,6 +65,9 @@ const backButton = window.Telegram.WebApp.BackButton;
|
|||||||
const haptic = window.Telegram.WebApp.HapticFeedback;
|
const haptic = window.Telegram.WebApp.HapticFeedback;
|
||||||
const drawerOpen = ref(false);
|
const drawerOpen = ref(false);
|
||||||
|
|
||||||
|
// Инициализация жеста Swipe Back (без визуального индикатора)
|
||||||
|
useSwipeBack();
|
||||||
|
|
||||||
const routesToHideAppDock = [
|
const routesToHideAppDock = [
|
||||||
'product.show',
|
'product.show',
|
||||||
'checkout',
|
'checkout',
|
||||||
|
|||||||
208
frontend/spa/src/composables/useSwipeBack.js
Normal file
208
frontend/spa/src/composables/useSwipeBack.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user