реорганизация приложения
This commit is contained in:
parent
2c452f9f20
commit
c61a15c8a7
351
src/App.vue
351
src/App.vue
@ -1,157 +1,344 @@
|
||||
<template>
|
||||
<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">
|
||||
<router-link to="/" class="navbar-brand text-white fs-4">DatingApp</router-link>
|
||||
<div>
|
||||
<template v-if="authState">
|
||||
<span class="me-3">Привет, {{ userData?.name || 'Пользователь' }}!</span>
|
||||
<router-link to="/swipe" class="btn btn-outline-light me-2">Поиск</router-link>
|
||||
<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 class="app-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="page-transition" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer mt-auto py-3 bg-light text-center">
|
||||
<div class="container">
|
||||
<span class="text-muted">© {{ new Date().getFullYear() }} Dating App PWA</span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Мобильная навигация внизу экрана -->
|
||||
<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>Поиск</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuth } from '@/auth'; // Убедись, что путь '@/auth' правильный
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useAuth } from '@/auth';
|
||||
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 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.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) => {
|
||||
console.log('isAuthenticated изменился:', newVal);
|
||||
console.log('[App] isAuthenticated изменился:', newVal);
|
||||
authState.value = newVal;
|
||||
|
||||
if (newVal) {
|
||||
fetchConversations();
|
||||
setupSocketConnection();
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем локальную копию данных пользователя
|
||||
watch(() => user.value, (newVal) => {
|
||||
console.log('Данные пользователя изменились:', 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.vue монтирован, проверка состояния аутентификации');
|
||||
console.log('[App] App.vue монтирован, проверка состояния аутентификации');
|
||||
authState.value = isAuthenticated.value;
|
||||
userData.value = user.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 {
|
||||
await logout();
|
||||
console.log('Пользователь успешно вышел из системы (из App.vue)');
|
||||
authState.value = false;
|
||||
userData.value = null;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выходе:', error);
|
||||
// При размонтировании компонента очищаем обработчики событий
|
||||
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, "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;
|
||||
background-color: #f8f9fa; /* Светлый фон для всего приложения */
|
||||
background-color: #f8f9fa;
|
||||
overscroll-behavior: none; /* Предотвращает "bounce" эффект на iOS */
|
||||
overflow: hidden;
|
||||
will-change: transform; /* Ускорение для анимаций */
|
||||
}
|
||||
|
||||
#app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Global Responsive Styles */
|
||||
.app {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
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 {
|
||||
flex-grow: 1;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
.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: 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) {
|
||||
.app-header .navbar-brand {
|
||||
font-size: 1.2rem;
|
||||
:root {
|
||||
--nav-height: 56px;
|
||||
}
|
||||
|
||||
.app-header .btn {
|
||||
min-width: 80px;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
.nav-item i {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small phones */
|
||||
/* Маленькие телефоны */
|
||||
@media (max-width: 375px) {
|
||||
.app-header .navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.app-header .btn {
|
||||
min-width: 70px;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
.nav-item span {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Landscape orientation on mobile */
|
||||
/* Ландшафтная ориентация на мобильных */
|
||||
@media (max-height: 450px) and (orientation: landscape) {
|
||||
.app-header {
|
||||
padding-top: 0.25rem !important;
|
||||
padding-bottom: 0.25rem !important;
|
||||
:root {
|
||||
--nav-height: 50px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</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
Loading…
x
Reference in New Issue
Block a user