2025-05-21 22:13:09 +07:00
|
|
|
|
<template>
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<div class="app-swipe-view">
|
|
|
|
|
<!-- Основное содержимое -->
|
2025-05-23 20:42:01 +07:00
|
|
|
|
<main class="swipe-content no-scroll">
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<!-- Loading State -->
|
|
|
|
|
<div v-if="loading" class="loading-section">
|
|
|
|
|
<div class="loading-spinner">
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
<p>Загрузка анкет...</p>
|
|
|
|
|
</div>
|
2025-05-21 22:13:09 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<!-- Error State -->
|
|
|
|
|
<div v-if="error" class="error-section">
|
2025-05-23 17:05:01 +07:00
|
|
|
|
<div class="error-card">
|
|
|
|
|
<i class="bi-exclamation-triangle"></i>
|
|
|
|
|
<h3>Ошибка загрузки</h3>
|
|
|
|
|
<p>{{ error }}</p>
|
|
|
|
|
<button class="retry-btn" @click="fetchSuggestions">Попробовать снова</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<!-- No Suggestions State -->
|
|
|
|
|
<div v-if="!loading && suggestions.length === 0 && !error" class="empty-section">
|
2025-05-23 17:05:01 +07:00
|
|
|
|
<div class="empty-card">
|
|
|
|
|
<i class="bi-person-x"></i>
|
|
|
|
|
<h3>Нет анкет</h3>
|
|
|
|
|
<p>Пока нет доступных анкет. Попробуй зайти позже!</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<!-- Main Card Swiping Area -->
|
|
|
|
|
<div v-if="!loading && suggestions.length > 0 && !error" class="swipe-area" ref="swipeArea">
|
|
|
|
|
<!-- Card Stack -->
|
|
|
|
|
<div class="card-stack">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(user, index) in visibleUsers"
|
|
|
|
|
:key="user._id"
|
|
|
|
|
class="swipe-card"
|
|
|
|
|
:class="{
|
|
|
|
|
'active': index === 0,
|
|
|
|
|
'next': index === 1,
|
|
|
|
|
'swiping-left': swipeDirection === 'left' && index === 0,
|
|
|
|
|
'swiping-right': swipeDirection === 'right' && index === 0
|
|
|
|
|
}"
|
|
|
|
|
:style="index === 0 ? cardStyle : {}"
|
|
|
|
|
@touchstart="index === 0 ? startTouch($event) : null"
|
|
|
|
|
@touchmove="index === 0 ? moveTouch($event) : null"
|
|
|
|
|
@touchend="index === 0 ? endTouch($event) : null"
|
2025-05-24 01:46:38 +07:00
|
|
|
|
@mousedown="index === 0 ? startDrag($event) : null"
|
|
|
|
|
@click="index === 0 ? handleCardClick($event, user) : null"
|
2025-05-23 18:42:29 +07:00
|
|
|
|
>
|
|
|
|
|
<!-- Photo Section -->
|
|
|
|
|
<div class="card-photo-section">
|
|
|
|
|
<!-- Photo Carousel -->
|
|
|
|
|
<div class="photo-carousel">
|
|
|
|
|
<!-- Photos Array -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="user.photos && user.photos.length > 0"
|
|
|
|
|
class="carousel-wrapper"
|
|
|
|
|
>
|
|
|
|
|
<div class="carousel-inner">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(photo, photoIndex) in user.photos"
|
|
|
|
|
:key="photo.public_id || photo._id"
|
|
|
|
|
class="carousel-slide"
|
|
|
|
|
:class="{ 'active': currentPhotoIndex[user._id] === photoIndex }"
|
2025-05-23 17:05:01 +07:00
|
|
|
|
>
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<img
|
|
|
|
|
:src="photo.url"
|
|
|
|
|
class="photo-image"
|
|
|
|
|
:alt="'Фото ' + user.name"
|
|
|
|
|
@error="onImageError($event, user, photo, 'carousel')"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-05-23 17:05:01 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<!-- Photo Navigation Dots -->
|
|
|
|
|
<div class="carousel-dots" v-if="user.photos.length > 1">
|
|
|
|
|
<span
|
|
|
|
|
v-for="(photo, photoIndex) in user.photos"
|
|
|
|
|
:key="`dot-${photo._id}`"
|
|
|
|
|
class="dot"
|
|
|
|
|
:class="{ 'active': currentPhotoIndex[user._id] === photoIndex }"
|
|
|
|
|
@click.stop="changePhoto(user._id, photoIndex)"
|
|
|
|
|
></span>
|
2025-05-23 17:05:01 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<!-- Previous/Next buttons -->
|
|
|
|
|
<button
|
|
|
|
|
v-if="user.photos.length > 1"
|
|
|
|
|
class="carousel-nav prev"
|
|
|
|
|
@click.stop="prevPhoto(user._id)"
|
2025-05-23 17:05:01 +07:00
|
|
|
|
>
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<i class="bi-chevron-left"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
v-if="user.photos.length > 1"
|
|
|
|
|
class="carousel-nav next"
|
|
|
|
|
@click.stop="nextPhoto(user._id)"
|
2025-05-23 17:05:01 +07:00
|
|
|
|
>
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<i class="bi-chevron-right"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Single Photo -->
|
|
|
|
|
<div v-else-if="user.mainPhotoUrl" class="single-photo">
|
|
|
|
|
<img
|
|
|
|
|
:src="user.mainPhotoUrl"
|
|
|
|
|
class="photo-image"
|
|
|
|
|
:alt="'Фото ' + user.name"
|
|
|
|
|
@error="onImageError($event, user, { url: user.mainPhotoUrl }, 'mainPhoto')"
|
|
|
|
|
/>
|
2025-05-23 17:05:01 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<!-- No Photo Placeholder -->
|
|
|
|
|
<div v-else class="no-photo">
|
|
|
|
|
<i class="bi-person"></i>
|
2025-05-23 17:05:01 +07:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
<!-- Action Indicators -->
|
|
|
|
|
<div class="swipe-indicators">
|
|
|
|
|
<div
|
|
|
|
|
class="like-indicator"
|
|
|
|
|
:class="{ 'visible': swipeDirection === 'right' && index === 0 }"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi-heart-fill"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="pass-indicator"
|
|
|
|
|
:class="{ 'visible': swipeDirection === 'left' && index === 0 }"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi-x-lg"></i>
|
2025-05-23 17:05:01 +07:00
|
|
|
|
</div>
|
2025-05-21 23:43:24 +07:00
|
|
|
|
</div>
|
2025-05-23 18:42:29 +07:00
|
|
|
|
|
|
|
|
|
<!-- User Info Overlay -->
|
|
|
|
|
<div class="user-info-overlay">
|
|
|
|
|
<h3 class="user-name">{{ user.name }}<span class="user-age">, {{ user.age || '?' }}</span></h3>
|
|
|
|
|
<p class="user-gender">{{ formatGender(user.gender) }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- User Details Section -->
|
|
|
|
|
<div class="card-details-section">
|
|
|
|
|
<div class="bio-container">
|
|
|
|
|
<p class="bio-text">{{ user.bio || 'Нет описания' }}</p>
|
|
|
|
|
</div>
|
2025-05-23 18:11:37 +07:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-05-21 22:13:09 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-23 20:42:01 +07:00
|
|
|
|
<!-- Убрали блок кнопок действий -->
|
2025-05-21 22:13:09 +07:00
|
|
|
|
</div>
|
2025-05-23 18:42:29 +07:00
|
|
|
|
</main>
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
|
|
|
|
<!-- Match Notification -->
|
|
|
|
|
<transition name="match-popup">
|
|
|
|
|
<div v-if="matchOccurred" class="match-popup">
|
|
|
|
|
<div class="match-content">
|
|
|
|
|
<div class="match-header">
|
|
|
|
|
<i class="bi-stars"></i>
|
|
|
|
|
<h3>Вы понравились друг другу!</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="match-actions">
|
|
|
|
|
<button class="match-btn secondary" @click="continueSearching">
|
|
|
|
|
Продолжить поиск
|
|
|
|
|
</button>
|
|
|
|
|
<button class="match-btn primary" @click="goToChats">
|
|
|
|
|
Начать общение
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</transition>
|
|
|
|
|
|
|
|
|
|
<!-- Background Overlay for Match -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="matchOccurred"
|
|
|
|
|
class="overlay"
|
|
|
|
|
@click="continueSearching"
|
|
|
|
|
></div>
|
2025-05-21 22:13:09 +07:00
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-05-23 17:05:01 +07:00
|
|
|
|
import { ref, onMounted, computed, reactive, watch, onUnmounted, nextTick } from 'vue';
|
|
|
|
|
import api from '@/services/api';
|
|
|
|
|
import { useAuth } from '@/auth';
|
|
|
|
|
import router from '@/router';
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
const { isAuthenticated, logout } = useAuth();
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
// Основные данные
|
2025-05-21 22:13:09 +07:00
|
|
|
|
const suggestions = ref([]);
|
|
|
|
|
const currentIndex = ref(0);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
const loading = ref(true);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
const actionLoading = ref(false);
|
|
|
|
|
const error = ref('');
|
|
|
|
|
const matchOccurred = ref(false);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
const matchedUserId = ref(null);
|
|
|
|
|
const swipeArea = ref(null);
|
|
|
|
|
|
|
|
|
|
// Для карусели фото
|
|
|
|
|
const currentPhotoIndex = reactive({});
|
|
|
|
|
|
|
|
|
|
// Для свайпа
|
|
|
|
|
const swipeDirection = ref(null);
|
2025-05-23 18:03:29 +07:00
|
|
|
|
const swipeThreshold = 80; // Минимальное расстояние для срабатывания свайпа
|
2025-05-23 17:05:01 +07:00
|
|
|
|
const cardStyle = ref({});
|
|
|
|
|
const swipeAngle = 12; // Угол наклона карты при свайпе (в градусах)
|
2025-05-23 18:03:29 +07:00
|
|
|
|
const isPanning = ref(false);
|
2025-05-23 18:17:31 +07:00
|
|
|
|
|
|
|
|
|
// Для touch событий
|
2025-05-23 18:11:37 +07:00
|
|
|
|
const touchStartX = ref(0);
|
|
|
|
|
const touchStartY = ref(0);
|
2025-05-23 18:17:31 +07:00
|
|
|
|
const touchEndX = ref(0);
|
|
|
|
|
const touchEndY = ref(0);
|
|
|
|
|
|
|
|
|
|
// Для mouse событий
|
|
|
|
|
const isDragging = ref(false);
|
|
|
|
|
const dragStartX = ref(0);
|
|
|
|
|
const dragStartY = ref(0);
|
|
|
|
|
const dragOffset = ref({ x: 0, y: 0 });
|
2025-05-24 01:31:04 +07:00
|
|
|
|
const dragStartTime = ref(0); // Добавляем время начала перетаскивания
|
|
|
|
|
const hasActuallyMoved = ref(false); // Флаг, что было реальное движение
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
// Вычисляемые свойства
|
2025-05-21 22:13:09 +07:00
|
|
|
|
const currentUserToSwipe = computed(() => {
|
|
|
|
|
if (suggestions.value.length > 0 && currentIndex.value < suggestions.value.length) {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
return suggestions.value[currentIndex.value];
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
const visibleUsers = computed(() => {
|
|
|
|
|
// Показываем текущего пользователя и следующего (для эффекта стека)
|
|
|
|
|
const users = [];
|
|
|
|
|
if (currentUserToSwipe.value) {
|
|
|
|
|
users.push(currentUserToSwipe.value);
|
|
|
|
|
|
|
|
|
|
if (currentIndex.value + 1 < suggestions.value.length) {
|
|
|
|
|
users.push(suggestions.value[currentIndex.value + 1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return users;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Методы управления каруселью фото
|
|
|
|
|
const initPhotoIndices = () => {
|
|
|
|
|
suggestions.value.forEach(user => {
|
|
|
|
|
if (user && user._id) {
|
|
|
|
|
if (!currentPhotoIndex[user._id]) {
|
|
|
|
|
// Определяем начальное фото (основное или первое в списке)
|
|
|
|
|
if (user.photos && user.photos.length > 0) {
|
|
|
|
|
const profilePhotoIndex = user.photos.findIndex(p => p.isProfilePhoto);
|
|
|
|
|
currentPhotoIndex[user._id] = profilePhotoIndex >= 0 ? profilePhotoIndex : 0;
|
|
|
|
|
} else {
|
|
|
|
|
currentPhotoIndex[user._id] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const changePhoto = (userId, index) => {
|
2025-05-23 18:03:29 +07:00
|
|
|
|
if (isPanning.value) return; // Не меняем фото во время свайпа
|
2025-05-23 17:05:01 +07:00
|
|
|
|
currentPhotoIndex[userId] = index;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const nextPhoto = (userId) => {
|
2025-05-23 18:03:29 +07:00
|
|
|
|
if (isPanning.value) return;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const prevPhoto = (userId) => {
|
2025-05-23 18:03:29 +07:00
|
|
|
|
if (isPanning.value) return;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
// Обработчики touch событий
|
|
|
|
|
const startTouch = (event) => {
|
2025-05-24 01:34:33 +07:00
|
|
|
|
// Проверяем, что это действительно touch-событие, а не эмуляция мыши
|
|
|
|
|
if (event.touches && event.touches.length === 1) {
|
|
|
|
|
isPanning.value = true;
|
|
|
|
|
touchStartX.value = event.touches[0].clientX;
|
|
|
|
|
touchStartY.value = event.touches[0].clientY;
|
2025-05-23 18:17:31 +07:00
|
|
|
|
|
2025-05-24 01:53:47 +07:00
|
|
|
|
// Сбросить конечные точки и время
|
2025-05-24 01:34:33 +07:00
|
|
|
|
touchEndX.value = 0;
|
|
|
|
|
touchEndY.value = 0;
|
2025-05-24 01:53:47 +07:00
|
|
|
|
dragStartTime.value = 0; // Сбрасываем время для touch событий
|
|
|
|
|
hasActuallyMoved.value = false;
|
2025-05-24 01:34:33 +07:00
|
|
|
|
}
|
2025-05-23 18:11:37 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
const moveTouch = (event) => {
|
2025-05-24 01:34:33 +07:00
|
|
|
|
if (!isPanning.value || !touchStartX.value || !event.touches || event.touches.length !== 1) return;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-23 18:11:37 +07:00
|
|
|
|
const currentX = event.touches[0].clientX;
|
|
|
|
|
const currentY = event.touches[0].clientY;
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
// Сохраняем текущую позицию
|
|
|
|
|
touchEndX.value = currentX;
|
|
|
|
|
touchEndY.value = currentY;
|
2025-05-23 18:11:37 +07:00
|
|
|
|
|
|
|
|
|
const diffX = currentX - touchStartX.value;
|
|
|
|
|
const diffY = currentY - touchStartY.value;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-24 01:34:33 +07:00
|
|
|
|
// Определяем направление свайпа для индикаторов только при значительном движении
|
|
|
|
|
if (Math.abs(diffX) > 10 && Math.abs(diffX) > Math.abs(diffY)) {
|
2025-05-23 18:11:37 +07:00
|
|
|
|
if (diffX > 20) {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
swipeDirection.value = 'right';
|
2025-05-23 18:11:37 +07:00
|
|
|
|
} else if (diffX < -20) {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
swipeDirection.value = 'left';
|
|
|
|
|
} else {
|
|
|
|
|
swipeDirection.value = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:03:29 +07:00
|
|
|
|
// Применяем небольшой поворот для естественности
|
2025-05-23 18:11:37 +07:00
|
|
|
|
const rotate = diffX * 0.05;
|
2025-05-23 17:42:27 +07:00
|
|
|
|
|
2025-05-23 18:03:29 +07:00
|
|
|
|
// Применяем точное смещение к карточке, она следует за пальцем 1 к 1
|
2025-05-23 17:05:01 +07:00
|
|
|
|
cardStyle.value = {
|
2025-05-23 18:11:37 +07:00
|
|
|
|
transform: `translateX(${diffX}px) rotate(${rotate}deg)`,
|
2025-05-23 17:05:01 +07:00
|
|
|
|
transition: 'none'
|
|
|
|
|
};
|
2025-05-23 18:11:37 +07:00
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
// Блокируем скролл страницы при горизонтальном свайпе
|
|
|
|
|
event.preventDefault();
|
2025-05-23 18:03:29 +07:00
|
|
|
|
}
|
2025-05-23 17:05:01 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
const endTouch = (event) => {
|
2025-05-23 18:11:37 +07:00
|
|
|
|
if (!isPanning.value) return;
|
2025-05-23 18:03:29 +07:00
|
|
|
|
isPanning.value = false;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-24 01:34:33 +07:00
|
|
|
|
// Проверяем, что у нас есть валидные координаты
|
|
|
|
|
if (!touchEndX.value || !touchStartX.value) {
|
|
|
|
|
// Если координаты отсутствуют, просто сбрасываем состояние
|
|
|
|
|
cardStyle.value = {
|
|
|
|
|
transform: 'none',
|
|
|
|
|
transition: 'transform 0.3s ease'
|
|
|
|
|
};
|
|
|
|
|
swipeDirection.value = null;
|
|
|
|
|
touchStartX.value = 0;
|
|
|
|
|
touchStartY.value = 0;
|
|
|
|
|
touchEndX.value = 0;
|
|
|
|
|
touchEndY.value = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
const diffX = touchEndX.value - touchStartX.value;
|
2025-05-23 18:11:37 +07:00
|
|
|
|
|
2025-05-23 18:03:29 +07:00
|
|
|
|
// Проверяем, достаточно ли было смещение для действия
|
2025-05-23 18:11:37 +07:00
|
|
|
|
if (Math.abs(diffX) > swipeThreshold) {
|
2025-05-23 18:03:29 +07:00
|
|
|
|
// Если свайп был достаточно сильным, выполняем соответствующее действие
|
2025-05-23 18:11:37 +07:00
|
|
|
|
if (diffX > 0) {
|
2025-05-23 18:03:29 +07:00
|
|
|
|
// Свайп вправо (лайк)
|
2025-05-23 17:53:15 +07:00
|
|
|
|
cardStyle.value = {
|
|
|
|
|
transform: `translateX(120%) rotate(${swipeAngle}deg)`,
|
|
|
|
|
transition: 'transform 0.3s ease'
|
|
|
|
|
};
|
|
|
|
|
// Плавная анимация уходящей карточки
|
|
|
|
|
setTimeout(() => handleLike(), 300);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
} else {
|
2025-05-23 18:03:29 +07:00
|
|
|
|
// Свайп влево (пропуск)
|
2025-05-23 17:53:15 +07:00
|
|
|
|
cardStyle.value = {
|
|
|
|
|
transform: `translateX(-120%) rotate(-${swipeAngle}deg)`,
|
|
|
|
|
transition: 'transform 0.3s ease'
|
|
|
|
|
};
|
|
|
|
|
// Плавная анимация уходящей карточки
|
|
|
|
|
setTimeout(() => handlePass(), 300);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-05-23 18:03:29 +07:00
|
|
|
|
// Если свайп недостаточный, возвращаем карточку в исходное положение с анимацией
|
2025-05-23 17:53:15 +07:00
|
|
|
|
cardStyle.value = {
|
|
|
|
|
transform: 'none',
|
|
|
|
|
transition: 'transform 0.3s ease'
|
|
|
|
|
};
|
2025-05-23 17:05:01 +07:00
|
|
|
|
swipeDirection.value = null;
|
|
|
|
|
}
|
2025-05-23 18:11:37 +07:00
|
|
|
|
|
|
|
|
|
// Сбрасываем значения
|
|
|
|
|
touchStartX.value = 0;
|
|
|
|
|
touchStartY.value = 0;
|
2025-05-23 18:17:31 +07:00
|
|
|
|
touchEndX.value = 0;
|
|
|
|
|
touchEndY.value = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Обработчики mouse событий
|
|
|
|
|
const startDrag = (event) => {
|
2025-05-24 01:31:04 +07:00
|
|
|
|
// Предотвращаем drag для изображений и других элементов
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
isDragging.value = true;
|
|
|
|
|
dragStartX.value = event.clientX;
|
|
|
|
|
dragStartY.value = event.clientY;
|
2025-05-24 01:31:04 +07:00
|
|
|
|
dragStartTime.value = Date.now(); // Записываем время начала
|
|
|
|
|
hasActuallyMoved.value = false; // Сбрасываем флаг движения
|
2025-05-23 18:17:31 +07:00
|
|
|
|
|
|
|
|
|
// Сброс смещения
|
|
|
|
|
dragOffset.value = { x: 0, y: 0 };
|
|
|
|
|
|
|
|
|
|
// Добавляем слушатели событий мыши на document
|
|
|
|
|
document.addEventListener('mousemove', moveDrag);
|
|
|
|
|
document.addEventListener('mouseup', endDrag);
|
2025-05-23 18:11:37 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
const moveDrag = (event) => {
|
|
|
|
|
if (!isDragging.value) return;
|
|
|
|
|
|
|
|
|
|
const currentX = event.clientX;
|
|
|
|
|
const currentY = event.clientY;
|
|
|
|
|
|
2025-05-24 01:31:04 +07:00
|
|
|
|
const newOffsetX = currentX - dragStartX.value;
|
|
|
|
|
const newOffsetY = currentY - dragStartY.value;
|
|
|
|
|
|
|
|
|
|
// Проверяем, было ли реальное движение (больше 3 пикселей)
|
|
|
|
|
if (Math.abs(newOffsetX) > 3 || Math.abs(newOffsetY) > 3) {
|
|
|
|
|
hasActuallyMoved.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
dragOffset.value = {
|
2025-05-24 01:31:04 +07:00
|
|
|
|
x: newOffsetX,
|
|
|
|
|
y: newOffsetY
|
2025-05-23 18:17:31 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-24 01:31:04 +07:00
|
|
|
|
// Применяем стили только если было реальное движение
|
|
|
|
|
if (hasActuallyMoved.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'
|
|
|
|
|
};
|
2025-05-23 18:17:31 +07:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-23 17:05:01 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-23 18:17:31 +07:00
|
|
|
|
const endDrag = () => {
|
|
|
|
|
if (!isDragging.value) return;
|
|
|
|
|
|
2025-05-24 01:31:04 +07:00
|
|
|
|
const dragDuration = Date.now() - dragStartTime.value;
|
|
|
|
|
const totalMovement = Math.sqrt(
|
|
|
|
|
Math.pow(dragOffset.value.x, 2) + Math.pow(dragOffset.value.y, 2)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Проверяем условия для выполнения действия:
|
|
|
|
|
// 1. Было реальное движение больше порога
|
|
|
|
|
// 2. Горизонтальное смещение больше вертикального
|
|
|
|
|
// 3. Общее движение больше минимального
|
|
|
|
|
// 4. Время перетаскивания больше минимального (избегаем случайных кликов)
|
|
|
|
|
const shouldTriggerAction = hasActuallyMoved.value &&
|
|
|
|
|
Math.abs(dragOffset.value.x) > swipeThreshold &&
|
|
|
|
|
Math.abs(dragOffset.value.x) > Math.abs(dragOffset.value.y) &&
|
|
|
|
|
totalMovement > 10 &&
|
|
|
|
|
dragDuration > 100; // Минимум 100ms для считывания как drag
|
|
|
|
|
|
|
|
|
|
if (shouldTriggerAction) {
|
2025-05-23 18:17:31 +07:00
|
|
|
|
// Если перетаскивание было достаточно большим, выполняем действие лайка/пропуска
|
|
|
|
|
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 {
|
2025-05-24 01:31:04 +07:00
|
|
|
|
// Если это был простой клик или недостаточное перетаскивание, возвращаем карточку в исходное положение
|
2025-05-23 18:17:31 +07:00
|
|
|
|
cardStyle.value = {
|
|
|
|
|
transform: 'none',
|
|
|
|
|
transition: 'transform 0.3s ease'
|
|
|
|
|
};
|
|
|
|
|
swipeDirection.value = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Сбрасываем состояние и удаляем слушатели
|
|
|
|
|
isDragging.value = false;
|
2025-05-24 01:31:04 +07:00
|
|
|
|
hasActuallyMoved.value = false;
|
|
|
|
|
dragStartTime.value = 0;
|
2025-05-23 18:17:31 +07:00
|
|
|
|
document.removeEventListener('mousemove', moveDrag);
|
|
|
|
|
document.removeEventListener('mouseup', endDrag);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-24 01:46:38 +07:00
|
|
|
|
// Обработчик клика на карточку
|
|
|
|
|
const handleCardClick = (event, user) => {
|
2025-05-24 01:50:38 +07:00
|
|
|
|
console.log('[SwipeView] handleCardClick вызван для пользователя:', user.name, user._id);
|
|
|
|
|
|
2025-05-24 01:46:38 +07:00
|
|
|
|
// Проверяем, что клик не был на элементах управления каруселью
|
|
|
|
|
if (event.target.closest('.carousel-nav') ||
|
|
|
|
|
event.target.closest('.carousel-dots') ||
|
|
|
|
|
event.target.closest('.dot')) {
|
|
|
|
|
console.log('[SwipeView] Клик на элементах управления каруселью - не открываем профиль');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-24 01:53:47 +07:00
|
|
|
|
// Проверяем, что в данный момент не происходит перетаскивание
|
|
|
|
|
if (isDragging.value || isPanning.value) {
|
|
|
|
|
console.log('[SwipeView] Клик проигнорирован - идет перетаскивание');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Проверяем, что было реальное движение только если dragStartTime был установлен
|
|
|
|
|
if (dragStartTime.value > 0) {
|
|
|
|
|
const dragDuration = Date.now() - dragStartTime.value;
|
|
|
|
|
const totalMovement = Math.sqrt(
|
|
|
|
|
Math.pow(dragOffset.value.x || 0, 2) + Math.pow(dragOffset.value.y || 0, 2)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log('[SwipeView] Проверка клика:', {
|
|
|
|
|
hasActuallyMoved: hasActuallyMoved.value,
|
|
|
|
|
totalMovement,
|
|
|
|
|
dragDuration,
|
|
|
|
|
dragOffset: dragOffset.value
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Если было реальное перетаскивание, не обрабатываем как клик
|
|
|
|
|
if (hasActuallyMoved.value || totalMovement > 10 || dragDuration < 300) {
|
|
|
|
|
console.log('[SwipeView] Клик проигнорирован - было перетаскивание');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Сбрасываем все состояния перетаскивания для чистого клика
|
|
|
|
|
dragStartTime.value = 0;
|
|
|
|
|
hasActuallyMoved.value = false;
|
|
|
|
|
dragOffset.value = { x: 0, y: 0 };
|
|
|
|
|
|
2025-05-24 01:46:38 +07:00
|
|
|
|
// Открываем полный профиль пользователя
|
2025-05-24 01:50:38 +07:00
|
|
|
|
console.log('[SwipeView] Переход к профилю пользователя:', user.name, user._id);
|
|
|
|
|
try {
|
|
|
|
|
router.push(`/user/${user._id}`);
|
|
|
|
|
console.log('[SwipeView] Маршрут установлен успешно');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[SwipeView] Ошибка при переходе к профилю:', error);
|
|
|
|
|
}
|
2025-05-24 01:46:38 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
// Основные функции
|
2025-05-21 22:13:09 +07:00
|
|
|
|
const fetchSuggestions = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
error.value = '';
|
|
|
|
|
matchOccurred.value = false;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
try {
|
|
|
|
|
console.log('[SwipeView] Запрос предложений...');
|
|
|
|
|
const response = await api.getSuggestions();
|
|
|
|
|
suggestions.value = response.data;
|
|
|
|
|
currentIndex.value = 0;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
console.log('[SwipeView] Предложения загружены:', suggestions.value);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
|
|
|
|
// Инициализируем индексы фото для карусели
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
initPhotoIndices();
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[SwipeView] Ошибка при загрузке предложений:', err.response ? err.response.data : err.message);
|
|
|
|
|
error.value = (err.response && err.response.data && err.response.data.message)
|
2025-05-23 17:05:01 +07:00
|
|
|
|
? err.response.data.message
|
|
|
|
|
: 'Не удалось загрузить анкеты. Попробуйте позже.';
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
if (err.response && err.response.status === 401) {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
await logout();
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAction = async (actionType) => {
|
|
|
|
|
if (!currentUserToSwipe.value || actionLoading.value) return;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
actionLoading.value = true;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
error.value = '';
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
if (actionType !== 'like') {
|
|
|
|
|
matchOccurred.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetUserId = currentUserToSwipe.value._id;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
try {
|
|
|
|
|
let response;
|
|
|
|
|
if (actionType === 'like') {
|
|
|
|
|
console.log(`[SwipeView] Лайк пользователя: ${targetUserId}`);
|
|
|
|
|
response = await api.likeUser(targetUserId);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[SwipeView] Пропуск пользователя: ${targetUserId}`);
|
|
|
|
|
response = await api.passUser(targetUserId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[SwipeView] Ответ на ${actionType}:`, response.data);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
if (response.data.isMatch) {
|
|
|
|
|
matchOccurred.value = true;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
matchedUserId.value = targetUserId;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
} else {
|
|
|
|
|
moveToNextUser();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(`[SwipeView] Ошибка при ${actionType}:`, err.response ? err.response.data : err.message);
|
|
|
|
|
error.value = `Ошибка при ${actionType === 'like' ? 'лайке' : 'пропуске'}. ${err.response?.data?.message || 'Попробуйте снова.'}`;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
if (err.response && err.response.status === 401) {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
await logout();
|
2025-05-21 22:13:09 +07:00
|
|
|
|
} else {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
setTimeout(() => { error.value = ''; }, 3000);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
actionLoading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleLike = () => {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
// Анимация свайпа вправо
|
|
|
|
|
swipeDirection.value = 'right';
|
|
|
|
|
cardStyle.value = {
|
|
|
|
|
transform: `translateX(120%) rotate(${swipeAngle}deg)`,
|
|
|
|
|
transition: 'transform 0.5s ease'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Задержка перед вызовом API
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
handleAction('like');
|
|
|
|
|
}, 300);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePass = () => {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
// Анимация свайпа влево
|
|
|
|
|
swipeDirection.value = 'left';
|
|
|
|
|
cardStyle.value = {
|
|
|
|
|
transform: `translateX(-120%) rotate(-${swipeAngle}deg)`,
|
|
|
|
|
transition: 'transform 0.5s ease'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Задержка перед вызовом API
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
handleAction('pass');
|
|
|
|
|
}, 300);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const moveToNextUser = () => {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
// Сбрасываем стили карточки
|
|
|
|
|
cardStyle.value = {
|
|
|
|
|
transform: 'none',
|
|
|
|
|
transition: 'none'
|
|
|
|
|
};
|
|
|
|
|
swipeDirection.value = null;
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
if (currentIndex.value < suggestions.value.length - 1) {
|
|
|
|
|
currentIndex.value++;
|
|
|
|
|
} else {
|
|
|
|
|
console.log('[SwipeView] Предложения закончились, перезагружаем...');
|
2025-05-23 17:05:01 +07:00
|
|
|
|
fetchSuggestions();
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
const continueSearching = () => {
|
|
|
|
|
matchOccurred.value = false;
|
|
|
|
|
moveToNextUser();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const goToChats = () => {
|
|
|
|
|
matchOccurred.value = false;
|
|
|
|
|
router.push('/chats');
|
|
|
|
|
};
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
|
|
|
|
const formatGender = (gender) => {
|
|
|
|
|
if (gender === 'male') return 'Мужчина';
|
|
|
|
|
if (gender === 'female') return 'Женщина';
|
|
|
|
|
if (gender === 'other') return 'Другой';
|
2025-05-23 17:05:01 +07:00
|
|
|
|
return '';
|
2025-05-21 22:13:09 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-22 00:15:54 +07:00
|
|
|
|
const onImageError = (event, userOnError, photoInfo, sourceType) => {
|
2025-05-22 00:09:16 +07:00
|
|
|
|
console.warn(
|
2025-05-22 00:15:54 +07:00
|
|
|
|
`SwipeView: Не удалось загрузить изображение (Источник: ${sourceType}).`,
|
2025-05-22 00:09:16 +07:00
|
|
|
|
{
|
|
|
|
|
src: event.target.src,
|
|
|
|
|
user: userOnError.name,
|
2025-05-22 00:15:54 +07:00
|
|
|
|
userId: userOnError._id,
|
|
|
|
|
photo_id: photoInfo._id,
|
|
|
|
|
photo_url: photoInfo.url,
|
|
|
|
|
isProfilePhotoFlag: photoInfo.isProfilePhoto,
|
2025-05-23 17:05:01 +07:00
|
|
|
|
event_type: event.type
|
2025-05-21 23:43:24 +07:00
|
|
|
|
}
|
2025-05-22 00:09:16 +07:00
|
|
|
|
);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-25 21:31:49 +07:00
|
|
|
|
// Добавляем функции для полного запрета скроллинга
|
|
|
|
|
const preventScroll = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const preventWheelScroll = (e) => {
|
|
|
|
|
// Запрещаем прокрутку колесом мыши
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const preventTouchScroll = (e) => {
|
|
|
|
|
// Разрешаем touch события только для карточек (свайп)
|
|
|
|
|
if (!e.target.closest('.swipe-card') && !e.target.closest('.carousel-nav') && !e.target.closest('.dot')) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
// Обработчики событий жизненного цикла
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
if (isAuthenticated.value) {
|
|
|
|
|
fetchSuggestions();
|
|
|
|
|
} else {
|
|
|
|
|
error.value = "Пожалуйста, войдите в систему, чтобы просматривать анкеты.";
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
2025-05-25 21:31:49 +07:00
|
|
|
|
|
|
|
|
|
// Добавляем обработчики для блокировки скроллинга
|
|
|
|
|
const swipeContentElement = document.querySelector('.swipe-content');
|
|
|
|
|
if (swipeContentElement) {
|
|
|
|
|
swipeContentElement.addEventListener('wheel', preventWheelScroll, { passive: false });
|
|
|
|
|
swipeContentElement.addEventListener('scroll', preventScroll, { passive: false });
|
|
|
|
|
swipeContentElement.addEventListener('touchmove', preventTouchScroll, { passive: false });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Блокируем скроллинг на уровне всего документа при нахождении в SwipeView
|
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
|
document.documentElement.style.overflow = 'hidden';
|
2025-05-23 17:05:01 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2025-05-23 18:17:31 +07:00
|
|
|
|
// Удаляем обработчики мыши при размонтировании компонента
|
|
|
|
|
document.removeEventListener('mousemove', moveDrag);
|
|
|
|
|
document.removeEventListener('mouseup', endDrag);
|
2025-05-25 21:31:49 +07:00
|
|
|
|
|
|
|
|
|
// Удаляем обработчики блокировки скроллинга
|
|
|
|
|
const swipeContentElement = document.querySelector('.swipe-content');
|
|
|
|
|
if (swipeContentElement) {
|
|
|
|
|
swipeContentElement.removeEventListener('wheel', preventWheelScroll);
|
|
|
|
|
swipeContentElement.removeEventListener('scroll', preventScroll);
|
|
|
|
|
swipeContentElement.removeEventListener('touchmove', preventTouchScroll);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Восстанавливаем скроллинг при выходе из компонента
|
|
|
|
|
document.body.style.overflow = '';
|
|
|
|
|
document.documentElement.style.overflow = '';
|
2025-05-23 17:05:01 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Следим за изменениями в списке предложений
|
|
|
|
|
watch(suggestions, () => {
|
|
|
|
|
initPhotoIndices();
|
|
|
|
|
}, { deep: true });
|
2025-05-24 02:10:15 +07:00
|
|
|
|
|
|
|
|
|
// Следим за изменением текущего пользователя для записи просмотров
|
|
|
|
|
watch(currentUserToSwipe, async (newUser, oldUser) => {
|
|
|
|
|
if (newUser && newUser._id && (!oldUser || newUser._id !== oldUser._id)) {
|
|
|
|
|
// Записываем просмотр профиля для нового пользователя
|
|
|
|
|
try {
|
|
|
|
|
await api.recordProfileView(newUser._id, 'swipe');
|
|
|
|
|
console.log('[SwipeView] Просмотр профиля записан для пользователя:', newUser.name, newUser._id);
|
|
|
|
|
} catch (viewError) {
|
|
|
|
|
console.warn('[SwipeView] Не удалось записать просмотр профиля:', viewError);
|
|
|
|
|
// Не показываем ошибку пользователю, так как это не критично
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-05-21 22:13:09 +07:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Основные стили контейнера */
|
|
|
|
|
.app-swipe-view {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
height: 100vh;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
width: 100%;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
position: relative;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
overscroll-behavior: none !important;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 20:42:01 +07:00
|
|
|
|
/* Блокировка прокрутки */
|
|
|
|
|
.no-scroll {
|
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
touch-action: none !important;
|
|
|
|
|
position: relative;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
overscroll-behavior: none !important;
|
|
|
|
|
-webkit-overflow-scrolling: auto !important;
|
2025-05-23 20:42:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Хедер */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.swipe-header {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
background: rgba(33, 33, 60, 0.9);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
backdrop-filter: blur(10px);
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 0.8rem 0;
|
|
|
|
|
text-align: center;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
color: white;
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0;
|
|
|
|
|
z-index: 10;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
height: var(--header-height, 56px);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
margin: 0;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
font-size: 1.3rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Основная область контента */
|
|
|
|
|
.swipe-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
position: relative;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
overscroll-behavior: none !important;
|
|
|
|
|
padding-bottom: var(--tabs-height, 56px);
|
|
|
|
|
/* Полная блокировка всех видов скроллинга */
|
|
|
|
|
-webkit-overflow-scrolling: auto !important;
|
|
|
|
|
-ms-overflow-style: none !important;
|
|
|
|
|
scrollbar-width: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Скрываем полосы прокрутки во всех браузерах */
|
|
|
|
|
.swipe-content::-webkit-scrollbar {
|
|
|
|
|
display: none !important;
|
|
|
|
|
width: 0 !important;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Состояния загрузки и ошибки */
|
|
|
|
|
.loading-section,
|
|
|
|
|
.error-section,
|
|
|
|
|
.empty-section {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 1rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
color: white;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
overflow: hidden !important;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-spinner {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spinner {
|
|
|
|
|
width: 50px;
|
|
|
|
|
height: 50px;
|
|
|
|
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
|
|
|
|
border-left: 4px solid white;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
margin: 0 auto 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
0% { transform: rotate(0deg); }
|
|
|
|
|
100% { transform: rotate(360deg); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error-card, .empty-card {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
background: white;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
border-radius: 20px;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 2rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
text-align: center;
|
|
|
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
2025-05-23 18:42:29 +07:00
|
|
|
|
max-width: 400px;
|
|
|
|
|
width: 100%;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error-card i, .empty-card i {
|
|
|
|
|
font-size: 3rem;
|
|
|
|
|
margin-bottom: 1rem;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
color: #495057;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error-card h3, .empty-card h3 {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
color: #343a40;
|
|
|
|
|
margin-bottom: 0.5rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.retry-btn {
|
|
|
|
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 0.8rem 2rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
border-radius: 50px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s ease;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
margin-top: 1rem;
|
|
|
|
|
display: inline-block;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Область свайпов */
|
|
|
|
|
.swipe-area {
|
|
|
|
|
flex: 1;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
position: relative;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 0.8rem;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
touch-action: none !important;
|
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
overscroll-behavior: none !important;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Стек карточек */
|
|
|
|
|
.card-stack {
|
|
|
|
|
flex: 1;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
position: relative;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
margin: 0 auto;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
width: 100%;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
max-width: 400px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
perspective: 1000px;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
overscroll-behavior: none !important;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Свайп-карточка */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.swipe-card {
|
|
|
|
|
position: absolute;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
height: calc(100% - 16px);
|
2025-05-23 18:42:29 +07:00
|
|
|
|
border-radius: 16px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
background-color: white;
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
2025-05-25 21:31:49 +07:00
|
|
|
|
overflow: hidden !important;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
will-change: transform;
|
|
|
|
|
transition: transform 0.3s ease, opacity 0.3s ease, scale 0.3s ease;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
user-select: none;
|
|
|
|
|
touch-action: none !important;
|
|
|
|
|
overscroll-behavior: none !important;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.swipe-card.active {
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.swipe-card.next {
|
|
|
|
|
z-index: 5;
|
|
|
|
|
transform: scale(0.95) translateY(15px);
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Секция с фотографией */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.card-photo-section {
|
|
|
|
|
flex: 1;
|
|
|
|
|
position: relative;
|
|
|
|
|
min-height: 0;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
max-height: 70vh;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
|
|
|
|
.photo-carousel {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
position: relative;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
|
|
|
|
.carousel-wrapper {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.carousel-inner {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.carousel-slide {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.5s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.carousel-slide.active {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.photo-image {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Навигация карусели */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.carousel-nav {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
background: rgba(255, 255, 255, 0.7);
|
|
|
|
|
border: none;
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
z-index: 5;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
transition: all 0.2s ease;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.carousel-nav.prev {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
left: 12px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.carousel-nav.next {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
right: 12px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.carousel-dots {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 15px;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
gap: 6px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
z-index: 5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dot {
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
cursor: pointer;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
transition: all 0.2s ease;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dot.active {
|
|
|
|
|
background-color: white;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
transform: scale(1.2);
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Заполнители для фото */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.single-photo {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-photo {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background-color: #e9ecef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-photo i {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
font-size: 4rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
color: #adb5bd;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Индикаторы действий */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.swipe-indicators {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.like-indicator, .pass-indicator {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%) scale(0);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
width: 90px;
|
|
|
|
|
height: 90px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
border-radius: 50%;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
font-size: 2.8rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
opacity: 0;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
|
|
|
|
.like-indicator {
|
|
|
|
|
right: 20%;
|
|
|
|
|
background-color: rgba(40, 167, 69, 0.8);
|
|
|
|
|
color: white;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
|
|
|
|
.pass-indicator {
|
|
|
|
|
left: 20%;
|
|
|
|
|
background-color: rgba(220, 53, 69, 0.8);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
color: white;
|
|
|
|
|
}
|
2025-05-23 17:05:01 +07:00
|
|
|
|
|
|
|
|
|
.like-indicator.visible, .pass-indicator.visible {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateY(-50%) scale(1);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Информация о пользователе */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.user-info-overlay {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
color: white;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 16px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
text-align: left;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.user-name {
|
|
|
|
|
margin: 0;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
font-size: 1.6rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.user-gender {
|
|
|
|
|
margin: 5px 0 0;
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Секция с деталями */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.card-details-section {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
background: white;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
|
|
|
|
flex-shrink: 0;
|
2025-05-25 21:31:49 +07:00
|
|
|
|
max-height: 100px;
|
|
|
|
|
overflow-y: auto;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Кнопки действий */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.action-buttons {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 20px;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 16px 0;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
width: 65px;
|
|
|
|
|
height: 65px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
border-radius: 50%;
|
|
|
|
|
border: none;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
background: white;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-icon {
|
2025-05-22 00:12:57 +07:00
|
|
|
|
width: 100%;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
height: 100%;
|
2025-05-22 00:12:57 +07:00
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
font-size: 1.8rem;
|
2025-05-22 00:12:57 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.pass-btn .btn-icon {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
color: #dc3545;
|
2025-05-22 00:12:57 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.like-btn .btn-icon {
|
2025-05-23 17:05:01 +07:00
|
|
|
|
color: #28a745;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn:disabled {
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Всплывающее окно совпадения */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.match-popup {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 20px;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 1.8rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
width: 90%;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
max-width: 360px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
|
|
|
|
|
text-align: center;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.match-header {
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.match-header i {
|
|
|
|
|
font-size: 3rem;
|
|
|
|
|
color: #ff6b6b;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.match-header h3 {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
font-size: 1.4rem;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
color: #333;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.match-actions {
|
|
|
|
|
display: flex;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.8rem;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
2025-05-22 00:04:45 +07:00
|
|
|
|
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.match-btn {
|
|
|
|
|
padding: 0.8rem 1rem;
|
|
|
|
|
border-radius: 50px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s ease;
|
2025-05-23 18:42:29 +07:00
|
|
|
|
width: 100%;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.match-btn.primary {
|
|
|
|
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.match-btn.secondary {
|
|
|
|
|
background: #f1f3f5;
|
|
|
|
|
color: #495057;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Затемняющий оверлей */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
|
|
|
backdrop-filter: blur(4px);
|
|
|
|
|
z-index: 999;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Анимации для всплывающего окна */
|
2025-05-23 17:05:01 +07:00
|
|
|
|
.match-popup-enter-active, .match-popup-leave-active {
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.match-popup-enter-from, .match-popup-leave-to {
|
|
|
|
|
transform: translate(-50%, -50%) scale(0.9);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Адаптивные настройки */
|
|
|
|
|
@media (min-height: 700px) {
|
|
|
|
|
.card-photo-section {
|
|
|
|
|
max-height: 65vh;
|
|
|
|
|
}
|
2025-05-23 18:03:29 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
@media (max-height: 600px) {
|
|
|
|
|
.card-photo-section {
|
|
|
|
|
max-height: 60vh;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-details-section {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 8px 12px;
|
2025-05-23 17:05:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bio-container {
|
|
|
|
|
max-height: 60px;
|
|
|
|
|
}
|
2025-05-23 17:13:32 +07:00
|
|
|
|
|
|
|
|
|
.action-buttons {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 10px 0;
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
width: 55px;
|
|
|
|
|
height: 55px;
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-icon {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
font-size: 1.5rem;
|
2025-05-23 17:53:15 +07:00
|
|
|
|
}
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
/* Ландшафтная ориентация */
|
|
|
|
|
@media (max-height: 450px) and (orientation: landscape) {
|
2025-05-23 17:13:32 +07:00
|
|
|
|
.swipe-area {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
flex-direction: row;
|
|
|
|
|
gap: 0.8rem;
|
|
|
|
|
padding: 0.5rem 0.8rem;
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.card-stack {
|
|
|
|
|
max-width: calc(100% - 130px);
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-buttons {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
padding: 16px 0;
|
|
|
|
|
justify-content: center;
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.swipe-header {
|
|
|
|
|
height: 46px;
|
|
|
|
|
padding: 0.5rem 0;
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.section-title {
|
|
|
|
|
font-size: 1.1rem;
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-details-section {
|
2025-05-23 18:42:29 +07:00
|
|
|
|
padding: 6px 10px;
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.bio-container {
|
|
|
|
|
max-height: 50px;
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.user-name {
|
|
|
|
|
font-size: 1.3rem;
|
|
|
|
|
}
|
2025-05-23 18:42:29 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Маленькие экраны */
|
|
|
|
|
@media (max-width: 375px) {
|
|
|
|
|
.user-name {
|
|
|
|
|
font-size: 1.4rem;
|
2025-05-23 17:53:15 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.action-btn {
|
|
|
|
|
width: 60px;
|
|
|
|
|
height: 60px;
|
2025-05-23 17:53:15 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.carousel-nav {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
2025-05-23 17:53:15 +07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 18:42:29 +07:00
|
|
|
|
.like-indicator, .pass-indicator {
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
font-size: 2.5rem;
|
2025-05-23 17:53:15 +07:00
|
|
|
|
}
|
2025-05-23 18:42:29 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Учитываем Safe Area для iPhone X+ */
|
|
|
|
|
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
|
|
|
|
.action-buttons {
|
|
|
|
|
padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
2025-05-23 17:13:32 +07:00
|
|
|
|
}
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
</style>
|