реорганизация приложения

This commit is contained in:
Professional 2025-05-23 18:42:29 +07:00
parent 2c452f9f20
commit c61a15c8a7
4 changed files with 1258 additions and 1736 deletions

View File

@ -1,157 +1,344 @@
<template> <template>
<div id="app-container"> <div id="app-container">
<header class="app-header bg-dark text-white p-3 mb-4 shadow-sm"> <!-- Основная область с содержимым -->
<nav class="container d-flex justify-content-between align-items-center"> <main class="app-content">
<router-link to="/" class="navbar-brand text-white fs-4">DatingApp</router-link> <router-view v-slot="{ Component }">
<div> <transition name="page-transition" mode="out-in">
<template v-if="authState"> <component :is="Component" />
<span class="me-3">Привет, {{ userData?.name || 'Пользователь' }}!</span> </transition>
<router-link to="/swipe" class="btn btn-outline-light me-2">Поиск</router-link> </router-view>
<router-link to="/chats" class="btn btn-outline-light me-2">Чаты</router-link> <!-- НОВАЯ ССЫЛКА -->
<router-link to="/profile" class="btn btn-outline-light me-2">Профиль</router-link>
<button @click="handleLogout" class="btn btn-light">Выход</button>
</template>
<template v-else>
<router-link to="/login" class="btn btn-outline-light me-2">Войти</router-link>
<router-link to="/register" class="btn btn-light">Регистрация</router-link>
</template>
</div>
</nav>
</header>
<main class="container">
<router-view /> <!-- Сюда Vue Router будет рендерить компонент текущего маршрута -->
</main> </main>
<footer class="app-footer mt-auto py-3 bg-light text-center"> <!-- Мобильная навигация внизу экрана -->
<div class="container"> <nav v-if="authState" class="mobile-nav">
<span class="text-muted">© {{ new Date().getFullYear() }} Dating App PWA</span> <router-link to="/swipe" class="nav-item" active-class="active">
</div> <div class="nav-icon">
</footer> <i class="bi-search"></i>
</div>
<span>Поиск</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>Чаты</span>
</router-link>
<router-link to="/profile" class="nav-item" active-class="active">
<div class="nav-icon">
<i class="bi-person"></i>
</div>
<span>Профиль</span>
</router-link>
</nav>
</div> </div>
</template> </template>
<script setup> <script setup>
import { useAuth } from '@/auth'; // Убедись, что путь '@/auth' правильный import { useAuth } from '@/auth';
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted, computed } from 'vue';
import api from '@/services/api';
import { getSocket, connectSocket } from '@/services/socketService';
const { user, isAuthenticated, logout, fetchUser } = useAuth(); const { user, isAuthenticated, logout, fetchUser } = useAuth();
// Используем локальные переменные для отслеживания состояния // Используем локальные переменные для отслеживания состояния
const userData = ref(null); const userData = ref(null);
const authState = ref(false); 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.getConversations();
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) => { watch(() => isAuthenticated.value, (newVal) => {
console.log('isAuthenticated изменился:', newVal); console.log('[App] isAuthenticated изменился:', newVal);
authState.value = newVal; authState.value = newVal;
if (newVal) {
fetchConversations();
setupSocketConnection();
}
}); });
// Обновляем локальную копию данных пользователя // Обновляем локальную копию данных пользователя
watch(() => user.value, (newVal) => { watch(() => user.value, (newVal) => {
console.log('Данные пользователя изменились:', newVal); console.log('[App] Данные пользователя изменились:', newVal);
userData.value = 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(() => { onMounted(() => {
console.log('App.vue монтирован, проверка состояния аутентификации'); console.log('[App] App.vue монтирован, проверка состояния аутентификации');
authState.value = isAuthenticated.value; authState.value = isAuthenticated.value;
userData.value = user.value; userData.value = user.value;
// Убедимся, что у нас есть актуальные данные пользователя // Убедимся, что у нас есть актуальные данные пользователя
if (localStorage.getItem('userToken') && !isAuthenticated.value) { if (localStorage.getItem('userToken') && !isAuthenticated.value) {
fetchUser(); fetchUser().then(() => {
if (isAuthenticated.value) {
fetchConversations();
setupSocketConnection();
}
});
} else if (isAuthenticated.value) {
fetchConversations();
setupSocketConnection();
} }
}); });
const handleLogout = async () => { // При размонтировании компонента очищаем обработчики событий
try { onUnmounted(() => {
await logout(); if (socket) {
console.log('Пользователь успешно вышел из системы (из App.vue)'); socket.off('getMessage', handleNewMessage);
authState.value = false; socket.off('messagesRead', handleMessagesRead);
userData.value = null;
} catch (error) { if (user.value?._id) {
console.error('Ошибка при выходе:', error); socket.emit('leaveNotificationRoom', user.value._id);
}
} }
}; });
</script> </script>
<style> <style>
/* Глобальные стили */ /* Глобальные стили */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body { html, body {
height: 100%; height: 100%;
width: 100%;
overflow-x: hidden;
-webkit-tap-highlight-color: transparent; /* Убирает фон при касании на мобильных */
} }
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #212529; color: #212529;
background-color: #f8f9fa; /* Светлый фон для всего приложения */ background-color: #f8f9fa;
overscroll-behavior: none; /* Предотвращает "bounce" эффект на iOS */
overflow: hidden;
will-change: transform; /* Ускорение для анимаций */
} }
#app-container { #app-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; height: 100vh;
} width: 100%;
/* Global Responsive Styles */
.app {
position: relative; position: relative;
min-height: 100vh; overflow: hidden;
background-color: #f8f9fa;
} }
.app-header .navbar-brand { /* Основная область содержимого */
font-weight: bold; .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;
} }
.app-header .btn { /* Анимация переходов между страницами */
min-width: 100px; /* Чтобы кнопки были примерно одинаковой ширины */ .page-transition-enter-active,
.page-transition-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
} }
main.container { .page-transition-enter-from,
flex-grow: 1; .page-transition-leave-to {
padding-top: 1rem; opacity: 0;
padding-bottom: 1rem; 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: 4px 0;
position: relative;
}
.nav-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 3px;
height: 28px;
}
.nav-item i {
font-size: 1.5rem;
transition: transform 0.2s ease;
}
.nav-item span {
font-size: 0.7rem;
font-weight: 500;
}
.nav-item.active {
color: #667eea;
}
.nav-item.active i {
transform: scale(1.15);
}
/* Индикатор непрочитанных сообщений */
.unread-badge {
position: absolute;
top: -5px;
right: -8px;
background-color: #ff6b6b;
color: white;
font-size: 0.6rem;
border-radius: 50%;
min-width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
box-shadow: 0 2px 5px rgba(255, 107, 107, 0.3);
border: 1px solid white;
}
/* CSS переменные для динамической настройки */
:root {
--nav-height: 60px;
--header-height: 56px;
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
}
/* Учет Safe Area на устройствах с вырезами (iPhone X и новее) */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.mobile-nav {
height: calc(var(--nav-height) + var(--safe-area-inset-bottom));
padding-bottom: var(--safe-area-inset-bottom);
}
}
/* Адаптивные настройки для различных устройств */
@media (max-width: 576px) { @media (max-width: 576px) {
.app-header .navbar-brand { :root {
font-size: 1.2rem; --nav-height: 56px;
} }
.app-header .btn { .nav-item i {
min-width: 80px; font-size: 1.3rem;
font-size: 0.9rem;
padding: 0.4rem 0.8rem;
}
.container {
padding-left: 15px;
padding-right: 15px;
} }
} }
/* Small phones */ /* Маленькие телефоны */
@media (max-width: 375px) { @media (max-width: 375px) {
.app-header .navbar-brand { .nav-item span {
font-size: 1.1rem; font-size: 0.65rem;
}
.app-header .btn {
min-width: 70px;
font-size: 0.85rem;
padding: 0.35rem 0.7rem;
} }
} }
/* Landscape orientation on mobile */ /* Ландшафтная ориентация на мобильных */
@media (max-height: 450px) and (orientation: landscape) { @media (max-height: 450px) and (orientation: landscape) {
.app-header { :root {
padding-top: 0.25rem !important; --nav-height: 50px;
padding-bottom: 0.25rem !important; }
.nav-item {
flex-direction: row;
gap: 5px;
}
.nav-icon {
margin-bottom: 0;
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff