Reflex/src/App.vue

462 lines
16 KiB
Vue
Raw Normal View History

2025-05-21 22:13:09 +07:00
<template>
<div id="app-container">
<!-- Основная область с содержимым -->
<main class="app-content">
<router-view v-slot="{ Component }">
<transition name="page-transition" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
2025-05-21 22:13:09 +07:00
</main>
<!-- Мобильная навигация внизу экрана -->
<nav v-if="authState" class="mobile-nav">
<router-link to="/swipe" class="nav-item" active-class="active">
<div class="nav-icon">
<i class="bi-search"></i>
</div>
2025-05-23 20:37:35 +07:00
<span class="nav-text">Поиск</span>
</router-link>
<router-link to="/chats" class="nav-item" active-class="active">
<div class="nav-icon">
<i class="bi-chat-dots"></i>
<span v-if="totalUnread > 0" class="unread-badge">
{{ totalUnread < 100 ? totalUnread : '99+' }}
</span>
</div>
2025-05-23 20:37:35 +07:00
<span class="nav-text">Чаты</span>
</router-link>
<router-link to="/profile" class="nav-item" active-class="active">
<div class="nav-icon">
<i class="bi-person"></i>
</div>
2025-05-23 20:37:35 +07:00
<span class="nav-text">Профиль</span>
</router-link>
</nav>
2025-05-21 22:13:09 +07:00
</div>
</template>
<script setup>
import { useAuth } from '@/auth';
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
2025-05-24 23:52:52 +07:00
import { useRoute } from 'vue-router';
import api from '@/services/api';
import { getSocket, connectSocket } from '@/services/socketService';
2025-05-21 22:13:09 +07:00
const { user, isAuthenticated, logout, fetchUser } = useAuth();
2025-05-24 23:52:52 +07:00
const route = useRoute();
2025-05-21 22:13:09 +07:00
// Используем локальные переменные для отслеживания состояния
const userData = ref(null);
const authState = ref(false);
const conversations = ref([]);
const totalUnread = computed(() => {
return conversations.value.reduce((sum, conv) => sum + (conv.unreadCount || 0), 0);
});
let socket = null;
// Получение списка диалогов для отображения счетчика непрочитанных сообщений
const fetchConversations = async () => {
if (!isAuthenticated.value) return;
try {
const response = await api.getUserConversations();
conversations.value = response.data;
console.log('[App] Загружено диалогов для счетчика уведомлений:', conversations.value.length);
} catch (err) {
console.error('[App] Ошибка при загрузке диалогов:', err);
}
};
// Обработчик новых сообщений
const handleNewMessage = (message) => {
2025-05-24 23:57:34 +07:00
console.log('[App] Получено новое сообщение:', {
sender: message.sender,
currentUser: user.value?._id,
conversationId: message.conversationId
});
2025-05-25 00:02:25 +07:00
// ВАЖНО: Правильно сравниваем ID отправителя
// message.sender может быть как объектом, так и строкой ID
const senderId = typeof message.sender === 'object' ? message.sender._id : message.sender;
2025-05-24 23:57:34 +07:00
// ВАЖНО: Увеличиваем счетчик ТОЛЬКО если сообщение пришло от другого пользователя
2025-05-25 00:02:25 +07:00
if (senderId !== user.value?._id) {
2025-05-25 00:05:52 +07:00
// Добавляем небольшую задержку, чтобы дать время событию messagesRead сработать
// если пользователь находится в том же чате
setTimeout(() => {
const conversation = conversations.value.find(c => c._id === message.conversationId);
if (conversation) {
// Проверяем текущий маршрут - если пользователь находится в этом чате,
// то сообщение уже могло быть автоматически помечено как прочитанное
const currentRoute = route.path;
const isInThisChat = currentRoute === `/chat/${message.conversationId}`;
if (!isInThisChat) {
// Увеличиваем счетчик только если пользователь НЕ находится в этом чате
conversation.unreadCount = (conversation.unreadCount || 0) + 1;
console.log('[App] Увеличен счетчик в навигации для диалога:', message.conversationId);
} else {
console.log('[App] Пользователь находится в этом чате, счетчик не увеличиваем');
}
} else {
// Если диалога нет в списке, обновим весь список
fetchConversations();
}
}, 100); // Задержка 100ms
2025-05-24 23:57:34 +07:00
} else {
console.log('[App] Сообщение от текущего пользователя, счетчик в навигации не изменяется');
}
};
// Обработчик прочитанных сообщений
const handleMessagesRead = ({ conversationId, readerId }) => {
2025-05-24 23:52:52 +07:00
console.log('[App] Получено событие messagesRead:', { conversationId, readerId, currentUserId: user.value?._id });
// Если текущий пользователь прочитал сообщения
if (readerId === user.value?._id) {
const conversation = conversations.value.find(c => c._id === conversationId);
if (conversation) {
2025-05-24 23:52:52 +07:00
console.log('[App] Сбрасываем счетчик для диалога:', conversationId);
conversation.unreadCount = 0;
}
}
};
2025-05-21 22:13:09 +07:00
2025-05-24 23:52:52 +07:00
// Дополнительный обработчик для синхронизации при переходе в чат
const handleRouteChange = () => {
// Обновляем данные диалогов при изменении маршрута
if (isAuthenticated.value) {
fetchConversations();
}
};
2025-05-21 22:13:09 +07:00
// Обновляем локальные переменные при изменении состояния аутентификации
watch(() => isAuthenticated.value, (newVal) => {
console.log('[App] isAuthenticated изменился:', newVal);
2025-05-21 22:13:09 +07:00
authState.value = newVal;
if (newVal) {
fetchConversations();
setupSocketConnection();
}
2025-05-21 22:13:09 +07:00
});
// Обновляем локальную копию данных пользователя
watch(() => user.value, (newVal) => {
console.log('[App] Данные пользователя изменились:', newVal);
2025-05-21 22:13:09 +07:00
userData.value = newVal;
});
2025-05-24 23:52:52 +07:00
// Следим за изменением маршрута для обновления счетчика
watch(() => route.path, (newPath, oldPath) => {
console.log('[App] Маршрут изменился:', { from: oldPath, to: newPath });
// Если переходим в чат или возвращаемся из чата, обновляем счетчики
if (newPath.startsWith('/chat/') || oldPath?.startsWith('/chat/')) {
setTimeout(() => {
fetchConversations();
}, 500); // Небольшая задержка для завершения операций чтения
}
});
// Настройка сокет-соединения
const setupSocketConnection = () => {
socket = getSocket();
if (!socket) {
socket = connectSocket();
}
if (socket) {
socket.on('getMessage', handleNewMessage);
socket.on('messagesRead', handleMessagesRead);
// Присоединение к комнате для уведомлений
if (user.value?._id) {
socket.emit('joinNotificationRoom', user.value._id);
}
}
};
2025-05-21 22:13:09 +07:00
// При монтировании компонента синхронизируем состояние
onMounted(() => {
console.log('[App] App.vue монтирован, проверка состояния аутентификации');
2025-05-21 22:13:09 +07:00
authState.value = isAuthenticated.value;
userData.value = user.value;
// Убедимся, что у нас есть актуальные данные пользователя
if (localStorage.getItem('userToken') && !isAuthenticated.value) {
fetchUser().then(() => {
if (isAuthenticated.value) {
2025-05-25 23:41:35 +07:00
console.log('[App] Пользователь загружен, проверка статуса администратора:', user.value?.isAdmin);
// Проверяем, является ли пользователь администратором
if (user.value?.isAdmin) {
console.log('[App] Пользователь является администратором, перенаправление на админ-панель');
// Если да, перенаправляем на административную панель
if (route.path !== '/admin') {
const router = route.matched[0]?.instances.default?.$router;
if (router) {
router.push('/admin');
}
}
} else {
// Для обычных пользователей
fetchConversations();
setupSocketConnection();
}
}
});
} else if (isAuthenticated.value) {
2025-05-25 23:41:35 +07:00
console.log('[App] Пользователь уже аутентифицирован, проверка статуса администратора:', user.value?.isAdmin);
// Проверяем, является ли пользователь администратором
if (user.value?.isAdmin) {
console.log('[App] Пользователь является администратором, перенаправление на админ-панель');
// Если да, перенаправляем на административную панель
if (route.path !== '/admin') {
const router = route.matched[0]?.instances.default?.$router;
if (router) {
router.push('/admin');
}
}
} else {
// Для обычных пользователей
fetchConversations();
setupSocketConnection();
}
2025-05-21 22:13:09 +07:00
}
});
// При размонтировании компонента очищаем обработчики событий
onUnmounted(() => {
if (socket) {
socket.off('getMessage', handleNewMessage);
socket.off('messagesRead', handleMessagesRead);
if (user.value?._id) {
socket.emit('leaveNotificationRoom', user.value._id);
}
2025-05-21 22:13:09 +07:00
}
});
2025-05-21 22:13:09 +07:00
</script>
<style>
/* Глобальные стили */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
2025-05-21 22:13:09 +07:00
html, body {
height: 100%;
width: 100%;
overflow-x: hidden;
-webkit-tap-highlight-color: transparent; /* Убирает фон при касании на мобильных */
2025-05-21 22:13:09 +07:00
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
2025-05-21 22:13:09 +07:00
color: #212529;
background-color: #f8f9fa;
overscroll-behavior: none; /* Предотвращает "bounce" эффект на iOS */
overflow: hidden;
will-change: transform; /* Ускорение для анимаций */
2025-05-21 22:13:09 +07:00
}
#app-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
position: relative;
overflow: hidden;
background-color: #f8f9fa;
2025-05-21 22:13:09 +07:00
}
/* Основная область содержимого */
.app-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch; /* Плавная прокрутка на iOS */
width: 100%;
height: calc(100% - var(--nav-height, 60px)); /* Высота за вычетом навбара */
position: absolute;
top: 0;
left: 0;
right: 0;
}
/* Анимация переходов между страницами */
.page-transition-enter-active,
.page-transition-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.page-transition-enter-from,
.page-transition-leave-to {
opacity: 0;
transform: translateY(10px);
}
/* Мобильная навигация */
.mobile-nav {
display: flex;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--nav-height, 60px);
background: white;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
2025-05-23 20:37:35 +07:00
justify-content: center;
text-decoration: none;
color: #6c757d;
transition: all 0.2s ease;
2025-05-23 20:37:35 +07:00
/* Удаляем отступы, они будут применляться по-другому */
padding: 0;
position: relative;
}
.nav-icon {
position: relative;
2025-05-23 20:37:35 +07:00
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 3px;
height: 28px;
}
/* Стили для индикатора непрочитанных сообщений в навигации */
.nav-icon .unread-badge {
position: absolute;
top: -8px;
right: -8px;
background: linear-gradient(135deg, #ff6b6b, #ff5252);
color: white;
font-size: 0.65rem;
font-weight: 700;
border-radius: 10px;
min-width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 5px;
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.4);
border: 2px solid white;
animation: pulse-notification 2s ease-in-out infinite;
z-index: 1;
}
/* Анимация пульсации для привлечения внимания */
@keyframes pulse-notification {
0%, 100% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.4);
}
50% {
transform: scale(1.1);
box-shadow: 0 3px 12px rgba(255, 107, 107, 0.6);
}
}
2025-05-23 20:37:35 +07:00
/* Оборачиваем текст для лучшего управления */
.nav-text {
font-size: 0.7rem;
font-weight: 500;
/* Критическое изменение: добавляем отступ чтобы поднять текст выше home indicator */
transform: translateY(-10px);
display: block;
}
/* Активная вкладка */
.nav-item.active {
color: #667eea;
}
.nav-item.active .nav-icon i {
color: #667eea;
transform: translateY(-2px);
}
/* CSS переменные для динамической настройки */
:root {
--nav-height: 60px;
--header-height: 56px;
2025-05-23 20:33:46 +07:00
--safe-area-bottom: 0px;
}
2025-05-23 20:37:35 +07:00
/* Обновленная поддержка для устройств с вырезом (iPhone X и новее) */
@supports (padding: env(safe-area-inset-bottom)) {
:root {
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
}
.mobile-nav {
height: calc(var(--nav-height) + var(--safe-area-bottom));
2025-05-23 20:37:35 +07:00
padding-bottom: var(--safe-area-bottom);
2025-05-23 20:28:18 +07:00
}
2025-05-23 20:33:46 +07:00
.nav-item {
2025-05-23 20:37:35 +07:00
/* Элемент занимает всю высоту панели, включая safe area */
height: 100%;
/* Добавляем флекс для размещения иконки и текста */
display: flex;
flex-direction: column;
2025-05-23 20:33:46 +07:00
justify-content: flex-start;
2025-05-23 20:37:35 +07:00
padding-top: 8px;
}
2025-05-23 20:37:35 +07:00
/* Важное изменение - переместить этот селектор за пределы @supports */
2025-05-23 20:33:46 +07:00
.app-content {
height: calc(100% - var(--nav-height) - var(--safe-area-bottom));
}
}
/* Адаптивные настройки для различных устройств */
@media (max-width: 576px) {
:root {
--nav-height: 56px;
}
.nav-item i {
font-size: 1.3rem;
}
}
/* Маленькие телефоны */
@media (max-width: 375px) {
.nav-item span {
font-size: 0.65rem;
}
}
/* Ландшафтная ориентация на мобильных */
@media (max-height: 450px) and (orientation: landscape) {
:root {
--nav-height: 50px;
}
.nav-item {
flex-direction: row;
gap: 5px;
}
.nav-icon {
margin-bottom: 0;
}
}
2025-05-21 22:13:09 +07:00
</style>