From d1b953a75a9759a5645136c7ff3528e452724112 Mon Sep 17 00:00:00 2001 From: Professional Date: Fri, 23 May 2025 18:03:29 +0700 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=81=D0=B2=D0=B0?= =?UTF-8?q?=D0=B9=D0=BF=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 37 ++++++- package.json | 4 +- src/main.js | 7 ++ src/views/SwipeView.vue | 221 +++++++++++----------------------------- 4 files changed, 105 insertions(+), 164 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8596527..cc36e84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,9 @@ "multer": "^1.4.5-lts.2", "socket.io-client": "^4.8.1", "vue": "^3.5.13", - "vue-router": "^4.5.1" + "vue-hammer": "^0.2.0", + "vue-router": "^4.5.1", + "vue-swipe": "^2.4.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", @@ -4148,6 +4150,15 @@ "dev": true, "license": "ISC" }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6864,6 +6875,15 @@ } } }, + "node_modules/vue-hammer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/vue-hammer/-/vue-hammer-0.2.0.tgz", + "integrity": "sha512-u/J/TniXJAI/0cL2t3y+ZsEflw7sBl6cWhdlO0qToCDvKN31gaxxBMzZokhJ/SFzI5p2EGO8g0bmXWkbG10rVw==", + "license": "MIT", + "dependencies": { + "hammerjs": "^2.0.4" + } + }, "node_modules/vue-router": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", @@ -6879,6 +6899,15 @@ "vue": "^3.2.0" } }, + "node_modules/vue-swipe": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/vue-swipe/-/vue-swipe-2.4.0.tgz", + "integrity": "sha512-0N5gOED0XF/KsyeF7mT+52J2hGdnHx47xiBbVlZjCxzOyLHWX03W2/G4VhC7bvlMZO7kMt9I/rMPwKZhmk1XTQ==", + "license": "MIT", + "dependencies": { + "wind-dom": "0.0.3" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -6987,6 +7016,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wind-dom": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wind-dom/-/wind-dom-0.0.3.tgz", + "integrity": "sha512-TlL8trn5lguxKs2O9smpnWC8Ti8rvWP1p7A+k+0DKP9mPRwFzYuijebE1I5BbbklAoBlBS1nwT8ng2DZI51tQg==", + "license": "MIT" + }, "node_modules/workbox-background-sync": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz", diff --git a/package.json b/package.json index 24b0afb..9bef6c5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "multer": "^1.4.5-lts.2", "socket.io-client": "^4.8.1", "vue": "^3.5.13", - "vue-router": "^4.5.1" + "vue-hammer": "^0.2.0", + "vue-router": "^4.5.1", + "vue-swipe": "^2.4.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", diff --git a/src/main.js b/src/main.js index cf51445..215af48 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,8 @@ import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import { useAuth } from './auth'; // 1. Импортируем useAuth +import { VueHammer } from 'vue3-hammer'; +import VueSwipe from 'vue-swipe'; // Импорт стилей Bootstrap import 'bootstrap/dist/css/bootstrap.min.css'; @@ -9,6 +11,11 @@ import 'bootstrap/dist/css/bootstrap.min.css'; // Создаем приложение const app = createApp(App); +// Регистрируем библиотеки для улучшенного свайпа +app.use(VueHammer); +app.component('swipe', VueSwipe.Swipe); +app.component('swipe-item', VueSwipe.SwipeItem); + // Используем роутер app.use(router); diff --git a/src/views/SwipeView.vue b/src/views/SwipeView.vue index f261837..ca8ac82 100644 --- a/src/views/SwipeView.vue +++ b/src/views/SwipeView.vue @@ -42,8 +42,12 @@
- -
+
@@ -160,8 +160,8 @@

{{ user.bio || 'Нет описания.' }}

-
- + +
@@ -240,17 +240,10 @@ const currentPhotoIndex = reactive({}); // Для свайпа const swipeDirection = ref(null); -const touchStartX = ref(0); -const touchStartY = ref(0); -const touchEndX = ref(0); -const touchEndY = ref(0); -const swipeThreshold = 100; // Минимальное расстояние для срабатывания свайпа -const isDragging = ref(false); -const dragStartX = ref(0); -const dragStartY = ref(0); -const dragOffset = ref({ x: 0, y: 0 }); +const swipeThreshold = 80; // Минимальное расстояние для срабатывания свайпа const cardStyle = ref({}); const swipeAngle = 12; // Угол наклона карты при свайпе (в градусах) +const isPanning = ref(false); // Вычисляемые свойства const currentUserToSwipe = computed(() => { @@ -291,12 +284,12 @@ const initPhotoIndices = () => { }; const changePhoto = (userId, index) => { - if (isDragging.value) return; // Не меняем фото во время свайпа + if (isPanning.value) return; // Не меняем фото во время свайпа currentPhotoIndex[userId] = index; }; const nextPhoto = (userId) => { - if (isDragging.value) return; + if (isPanning.value) return; const user = suggestions.value.find(u => u._id === userId); if (user && user.photos && user.photos.length > 0) { currentPhotoIndex[userId] = (currentPhotoIndex[userId] + 1) % user.photos.length; @@ -304,65 +297,55 @@ const nextPhoto = (userId) => { }; const prevPhoto = (userId) => { - if (isDragging.value) return; + if (isPanning.value) return; const user = suggestions.value.find(u => u._id === userId); if (user && user.photos && user.photos.length > 0) { currentPhotoIndex[userId] = (currentPhotoIndex[userId] - 1 + user.photos.length) % user.photos.length; } }; -// Методы для свайпа на мобильных -const startTouch = (event) => { - touchStartX.value = event.touches[0].clientX; - touchStartY.value = event.touches[0].clientY; - swipeDirection.value = null; -}; - -const moveTouch = (event) => { - if (!touchStartX.value) return; +// Обработчики жестов для Hammer.js +const onPan = (event) => { + isPanning.value = true; - const currentX = event.touches[0].clientX; - const currentY = event.touches[0].clientY; + // Получаем смещение по x и y + const deltaX = event.deltaX; + const deltaY = event.deltaY; - // Сохраняем текущую позицию для использования в endTouch - touchEndX.value = currentX; - touchEndY.value = currentY; - - const diffX = currentX - touchStartX.value; - const diffY = currentY - touchStartY.value; - - // Применяем точное смещение к карточке, чтобы она следовала за пальцем - // Нет дополнительных коэффициентов, карточка перемещается точно на то же расстояние, что и палец - if (Math.abs(diffX) > Math.abs(diffY)) { - // Определяем направление свайпа для индикаторов - if (diffX > 20) { + // Определяем направление свайпа для индикаторов + if (Math.abs(deltaX) > Math.abs(deltaY)) { + if (deltaX > 20) { swipeDirection.value = 'right'; - } else if (diffX < -20) { + } else if (deltaX < -20) { swipeDirection.value = 'left'; } else { swipeDirection.value = null; } - // Применяем небольшой поворот для естественности, но меньший чем был - // 0.05 вместо 0.1, чтобы эффект был легче - const rotate = diffX * 0.05; + // Применяем небольшой поворот для естественности + const rotate = deltaX * 0.05; + // Применяем точное смещение к карточке, она следует за пальцем 1 к 1 cardStyle.value = { - transform: `translateX(${diffX}px) rotate(${rotate}deg)`, + transform: `translateX(${deltaX}px) rotate(${rotate}deg)`, transition: 'none' }; - - // Блокируем скролл страницы - event.preventDefault(); + } + + // Блокируем скролл страницы при горизонтальном свайпе + if (Math.abs(deltaX) > Math.abs(deltaY)) { + event.srcEvent.preventDefault(); } }; -const endTouch = () => { - const diffX = touchEndX.value - touchStartX.value; +const onPanEnd = (event) => { + isPanning.value = false; - if (Math.abs(diffX) > swipeThreshold) { - // Если свайп был достаточно длинный, анимируем к краю экрана - if (diffX > 0) { + // Проверяем, достаточно ли было смещение для действия + if (Math.abs(event.deltaX) > swipeThreshold) { + // Если свайп был достаточно сильным, выполняем соответствующее действие + if (event.deltaX > 0) { + // Свайп вправо (лайк) cardStyle.value = { transform: `translateX(120%) rotate(${swipeAngle}deg)`, transition: 'transform 0.3s ease' @@ -370,6 +353,7 @@ const endTouch = () => { // Плавная анимация уходящей карточки setTimeout(() => handleLike(), 300); } else { + // Свайп влево (пропуск) cardStyle.value = { transform: `translateX(-120%) rotate(-${swipeAngle}deg)`, transition: 'transform 0.3s ease' @@ -385,98 +369,11 @@ const endTouch = () => { }; swipeDirection.value = null; } - - // Сбрасываем значения - touchStartX.value = 0; - touchStartY.value = 0; - touchEndX.value = 0; - touchEndY.value = 0; }; -// Методы для свайпа на десктопе (drag) -const startDrag = (event) => { - isDragging.value = true; - dragStartX.value = event.clientX; - dragStartY.value = event.clientY; - dragOffset.value = { x: 0, y: 0 }; - swipeDirection.value = null; - - // Предотвращаем выделение текста - event.preventDefault(); -}; - -const moveDrag = (event) => { - if (!isDragging.value) return; - - dragOffset.value = { - x: event.clientX - dragStartX.value, - y: event.clientY - dragStartY.value - }; - - // Определяем направление свайпа для индикаторов - if (Math.abs(dragOffset.value.x) > Math.abs(dragOffset.value.y)) { - if (dragOffset.value.x > 20) { - swipeDirection.value = 'right'; - } else if (dragOffset.value.x < -20) { - swipeDirection.value = 'left'; - } else { - swipeDirection.value = null; - } - - // Применяем небольшой поворот для естественности (уменьшен) - const rotate = dragOffset.value.x * 0.05; - - // Карточка следует точно за курсором, без дополнительных коэффициентов - cardStyle.value = { - transform: `translateX(${dragOffset.value.x}px) rotate(${rotate}deg)`, - transition: 'none' - }; - } - - event.preventDefault(); -}; - -const endDrag = () => { - if (!isDragging.value) return; - - if (Math.abs(dragOffset.value.x) > swipeThreshold) { - // Если перетаскивание было достаточно большим, выполняем действие лайка/пропуска с анимацией - if (dragOffset.value.x > 0) { - cardStyle.value = { - transform: `translateX(120%) rotate(${swipeAngle}deg)`, - transition: 'transform 0.3s ease' - }; - // Плавная анимация уходящей карточки - setTimeout(() => handleLike(), 300); - } else { - cardStyle.value = { - transform: `translateX(-120%) rotate(-${swipeAngle}deg)`, - transition: 'transform 0.3s ease' - }; - // Плавная анимация уходящей карточки - setTimeout(() => handlePass(), 300); - } - } else { - // Если перетаскивание небольшое, плавно возвращаем карточку в исходное положение - cardStyle.value = { - transform: 'none', - transition: 'transform 0.3s ease' - }; - swipeDirection.value = null; - } - - isDragging.value = false; -}; - -const cancelDrag = () => { - if (isDragging.value) { - cardStyle.value = { - transform: 'none', - transition: 'transform 0.3s ease' - }; - swipeDirection.value = null; - isDragging.value = false; - } +const handleCardTap = (event) => { + // Обрабатываем обычный тап по карточке, если нужно + // Например, можно открыть полную информацию о пользователе }; // Основные функции @@ -641,22 +538,10 @@ onMounted(() => { error.value = "Пожалуйста, войдите в систему, чтобы просматривать анкеты."; loading.value = false; } - - // Предотвращаем скролл страницы при свайпе - if (swipeArea.value) { - swipeArea.value.addEventListener('touchmove', (e) => { - if (isDragging.value) { - e.preventDefault(); - } - }, { passive: false }); - } }); onUnmounted(() => { - // Удаляем слушатели событий - if (swipeArea.value) { - swipeArea.value.removeEventListener('touchmove', null); - } + // Здесь можно выполнить очистку, если необходимо }); // Следим за изменениями в списке предложений @@ -1250,6 +1135,18 @@ watch(suggestions, () => { opacity: 0; } +/* Добавляем стили для Swipe компонентов */ +.swipe { + height: 100%; + width: 100%; + overflow: visible; +} + +.swipe-item { + height: 100%; + width: 100%; +} + /* Responsive Adjustments */ @media (max-width: 576px) { .swipe-area {