реорганизация приложения
This commit is contained in:
parent
2c452f9f20
commit
c61a15c8a7
351
src/App.vue
351
src/App.vue
@ -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
Loading…
x
Reference in New Issue
Block a user