Reflex/src/App.vue
2025-05-23 20:37:35 +07:00

336 lines
9.8 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 api from '@/services/api';
import { getSocket, connectSocket } from '@/services/socketService';
const { user, isAuthenticated, logout, fetchUser } = useAuth();
// Используем локальные переменные для отслеживания состояния
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) => {
// Если сообщение от другого пользователя, увеличиваем счетчик
if (message.sender !== user.value?._id) {
const conversation = conversations.value.find(c => c._id === message.conversationId);
if (conversation) {
conversation.unreadCount = (conversation.unreadCount || 0) + 1;
} else {
// Если диалога нет в списке, обновим весь список
fetchConversations();
}
}
};
// Обработчик прочитанных сообщений
const handleMessagesRead = ({ conversationId, readerId }) => {
// Если текущий пользователь прочитал сообщения
if (readerId === user.value?._id) {
const conversation = conversations.value.find(c => c._id === conversationId);
if (conversation) {
conversation.unreadCount = 0;
}
}
};
// Обновляем локальные переменные при изменении состояния аутентификации
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;
});
// Настройка сокет-соединения
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) {
fetchConversations();
setupSocketConnection();
}
});
} else if (isAuthenticated.value) {
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-text {
font-size: 0.7rem;
font-weight: 500;
/* Критическое изменение: добавляем отступ чтобы поднять текст выше home indicator */
transform: translateY(-10px);
display: block;
}
/* 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>