Reflex/src/App.vue
Professional 417b243269 фикс
2025-05-25 23:41:35 +07:00

462 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
</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>
<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>
<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>
<span class="nav-text">Профиль</span>
</router-link>
</nav>
</div>
</template>
<script setup>
import { useAuth } from '@/auth';
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
import { useRoute } from 'vue-router';
import api from '@/services/api';
import { getSocket, connectSocket } from '@/services/socketService';
const { user, isAuthenticated, logout, fetchUser } = useAuth();
const route = useRoute();
// Используем локальные переменные для отслеживания состояния
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) => {
console.log('[App] Получено новое сообщение:', {
sender: message.sender,
currentUser: user.value?._id,
conversationId: message.conversationId
});
// ВАЖНО: Правильно сравниваем ID отправителя
// message.sender может быть как объектом, так и строкой ID
const senderId = typeof message.sender === 'object' ? message.sender._id : message.sender;
// ВАЖНО: Увеличиваем счетчик ТОЛЬКО если сообщение пришло от другого пользователя
if (senderId !== user.value?._id) {
// Добавляем небольшую задержку, чтобы дать время событию 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
} else {
console.log('[App] Сообщение от текущего пользователя, счетчик в навигации не изменяется');
}
};
// Обработчик прочитанных сообщений
const handleMessagesRead = ({ conversationId, readerId }) => {
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) {
console.log('[App] Сбрасываем счетчик для диалога:', conversationId);
conversation.unreadCount = 0;
}
}
};
// Дополнительный обработчик для синхронизации при переходе в чат
const handleRouteChange = () => {
// Обновляем данные диалогов при изменении маршрута
if (isAuthenticated.value) {
fetchConversations();
}
};
// Обновляем локальные переменные при изменении состояния аутентификации
watch(() => isAuthenticated.value, (newVal) => {
console.log('[App] isAuthenticated изменился:', newVal);
authState.value = newVal;
if (newVal) {
fetchConversations();
setupSocketConnection();
}
});
// Обновляем локальную копию данных пользователя
watch(() => user.value, (newVal) => {
console.log('[App] Данные пользователя изменились:', newVal);
userData.value = newVal;
});
// Следим за изменением маршрута для обновления счетчика
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);
}
}
};
// При монтировании компонента синхронизируем состояние
onMounted(() => {
console.log('[App] App.vue монтирован, проверка состояния аутентификации');
authState.value = isAuthenticated.value;
userData.value = user.value;
// Убедимся, что у нас есть актуальные данные пользователя
if (localStorage.getItem('userToken') && !isAuthenticated.value) {
fetchUser().then(() => {
if (isAuthenticated.value) {
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) {
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();
}
}
});
// При размонтировании компонента очищаем обработчики событий
onUnmounted(() => {
if (socket) {
socket.off('getMessage', handleNewMessage);
socket.off('messagesRead', handleMessagesRead);
if (user.value?._id) {
socket.emit('leaveNotificationRoom', user.value._id);
}
}
});
</script>
<style>
/* Глобальные стили */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
overflow-x: hidden;
-webkit-tap-highlight-color: transparent; /* Убирает фон при касании на мобильных */
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #212529;
background-color: #f8f9fa;
overscroll-behavior: none; /* Предотвращает "bounce" эффект на iOS */
overflow: hidden;
will-change: transform; /* Ускорение для анимаций */
}
#app-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
position: relative;
overflow: hidden;
background-color: #f8f9fa;
}
/* Основная область содержимого */
.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;
justify-content: center;
text-decoration: none;
color: #6c757d;
transition: all 0.2s ease;
/* Удаляем отступы, они будут применляться по-другому */
padding: 0;
position: relative;
}
.nav-icon {
position: relative;
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);
}
}
/* Оборачиваем текст для лучшего управления */
.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;
--safe-area-bottom: 0px;
}
/* Обновленная поддержка для устройств с вырезом (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));
padding-bottom: var(--safe-area-bottom);
}
.nav-item {
/* Элемент занимает всю высоту панели, включая safe area */
height: 100%;
/* Добавляем флекс для размещения иконки и текста */
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-top: 8px;
}
/* Важное изменение - переместить этот селектор за пределы @supports */
.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;
}
}
</style>