реорганизация приложения
This commit is contained in:
parent
2c452f9f20
commit
c61a15c8a7
349
src/App.vue
349
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 class="nav-icon">
|
||||||
|
<i class="bi-search"></i>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
<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
@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modern-chat-view">
|
<div class="app-chat-view">
|
||||||
<!-- Header Section -->
|
<!-- Фиксированный хедер -->
|
||||||
<div v-if="conversationData && !loadingInitialMessages" class="chat-header">
|
<div v-if="conversationData && !loadingInitialMessages" class="chat-header">
|
||||||
<div class="container">
|
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="chat-info">
|
|
||||||
<router-link to="/chats" class="back-button">
|
<router-link to="/chats" class="back-button">
|
||||||
<i class="bi-arrow-left"></i>
|
<i class="bi-arrow-left"></i>
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -32,9 +30,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<!-- Основное содержимое -->
|
||||||
|
<main class="chat-content">
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="loadingInitialMessages" class="loading-section">
|
<div v-if="loadingInitialMessages" class="loading-section">
|
||||||
<div class="loading-spinner">
|
<div class="loading-spinner">
|
||||||
@ -45,7 +43,6 @@
|
|||||||
|
|
||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
<div v-if="error" class="error-section">
|
<div v-if="error" class="error-section">
|
||||||
<div class="container">
|
|
||||||
<div class="error-card">
|
<div class="error-card">
|
||||||
<i class="bi-exclamation-triangle"></i>
|
<i class="bi-exclamation-triangle"></i>
|
||||||
<h3>Ошибка загрузки</h3>
|
<h3>Ошибка загрузки</h3>
|
||||||
@ -53,11 +50,9 @@
|
|||||||
<button class="retry-btn" @click="fetchInitialMessages">Попробовать снова</button>
|
<button class="retry-btn" @click="fetchInitialMessages">Попробовать снова</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No Data State -->
|
<!-- No Data State -->
|
||||||
<div v-if="!loadingInitialMessages && !conversationData && !error" class="empty-section">
|
<div v-if="!loadingInitialMessages && !conversationData && !error" class="empty-section">
|
||||||
<div class="container">
|
|
||||||
<div class="empty-card">
|
<div class="empty-card">
|
||||||
<i class="bi-chat-dots"></i>
|
<i class="bi-chat-dots"></i>
|
||||||
<h3>Диалог недоступен</h3>
|
<h3>Диалог недоступен</h3>
|
||||||
@ -65,14 +60,9 @@
|
|||||||
<router-link to="/chats" class="action-btn primary">К списку диалогов</router-link>
|
<router-link to="/chats" class="action-btn primary">К списку диалогов</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div v-if="conversationData && !loadingInitialMessages && !error" class="main-content">
|
|
||||||
<div class="container">
|
|
||||||
<div class="chat-card">
|
|
||||||
<!-- Messages Container -->
|
<!-- Messages Container -->
|
||||||
<div ref="messagesContainer" class="messages-container">
|
<div v-if="conversationData && !loadingInitialMessages && !error" ref="messagesContainer" class="messages-container">
|
||||||
<template v-for="item in messagesWithDateSeparators" :key="item.id">
|
<template v-for="item in messagesWithDateSeparators" :key="item.id">
|
||||||
<!-- Date Separator -->
|
<!-- Date Separator -->
|
||||||
<div v-if="item.type === 'date_separator'" class="date-separator">
|
<div v-if="item.type === 'date_separator'" class="date-separator">
|
||||||
@ -125,9 +115,10 @@
|
|||||||
<i class="bi-arrow-down"></i>
|
<i class="bi-arrow-down"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<!-- Message Input Area -->
|
<!-- Фиксированный нижний блок для ввода сообщений -->
|
||||||
<div v-if="isAuthenticated" class="message-input-container">
|
<div v-if="isAuthenticated && conversationData" class="message-input-container">
|
||||||
<form @submit.prevent="editingMessageId ? saveEditedMessage() : sendMessage()" class="message-form">
|
<form @submit.prevent="editingMessageId ? saveEditedMessage() : sendMessage()" class="message-form">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -159,9 +150,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Context Menu (Mobile) -->
|
<!-- Context Menu (Mobile) -->
|
||||||
<div v-if="contextMenu && contextMenu.visible"
|
<div v-if="contextMenu && contextMenu.visible"
|
||||||
@ -183,12 +171,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// Используем существующий script setup
|
// Используем существующий script setup без изменений
|
||||||
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
|
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import api from '@/services/api';
|
import api from '@/services/api';
|
||||||
import { useAuth } from '@/auth';
|
import { useAuth } from '@/auth';
|
||||||
import { getSocket, connectSocket, disconnectSocket } from '@/services/socketService';
|
import { getSocket, connectSocket } from '@/services/socketService';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -691,50 +679,39 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Global Reset and Base Styles */
|
/* Основные стили */
|
||||||
.modern-chat-view {
|
.app-chat-view {
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
background-attachment: fixed; /* Фиксирует фон для растяжения на весь экран */
|
|
||||||
background-size: cover;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
height: 100vh;
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 15px;
|
position: relative;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header Section */
|
/* Хедер */
|
||||||
.chat-header {
|
.chat-header {
|
||||||
background: rgba(33, 33, 60, 0.8); /* Более темный, менее прозрачный фон */
|
background: rgba(33, 33, 60, 0.9);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
padding: 1rem 0;
|
padding: 0.8rem 1rem;
|
||||||
color: white;
|
color: white;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: var(--header-height, 56px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-info {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
@ -747,22 +724,17 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
transform: translateX(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Аватар участника (новый) */
|
|
||||||
.participant-avatar {
|
.participant-avatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-image {
|
.avatar-image {
|
||||||
@ -786,29 +758,28 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.participant-info {
|
.participant-info {
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.participant-name {
|
.participant-name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); /* Добавлена тень для лучшей видимости текста */
|
white-space: nowrap;
|
||||||
color: #ffffff; /* Гарантирует белый цвет текста */
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typing-status {
|
.typing-status {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
opacity: 0.8;
|
opacity: 0.9;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Анимация "печатает" */
|
|
||||||
.typing-animation {
|
.typing-animation {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
@ -837,18 +808,29 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
50% { transform: translateY(-4px); }
|
50% { transform: translateY(-4px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading State */
|
/* Основная область содержимого */
|
||||||
.loading-section {
|
.chat-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Состояния загрузки и ошибки */
|
||||||
|
.loading-section,
|
||||||
|
.error-section,
|
||||||
|
.empty-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 400px;
|
flex: 1;
|
||||||
flex-grow: 1;
|
padding: 1rem;
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
@ -877,22 +859,19 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error and Empty States */
|
.error-card,
|
||||||
.error-section, .empty-section {
|
.empty-card {
|
||||||
padding: 4rem 0;
|
background: white;
|
||||||
flex-grow: 1;
|
border-radius: 16px;
|
||||||
}
|
padding: 2rem;
|
||||||
|
|
||||||
.error-card, .empty-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 3rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-card i, .empty-card i {
|
.error-card i,
|
||||||
|
.empty-card i {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@ -905,71 +884,46 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-card h3, .empty-card h3 {
|
.error-card h3,
|
||||||
color: #495057;
|
.empty-card h3 {
|
||||||
|
color: #343a40;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.retry-btn {
|
.retry-btn,
|
||||||
|
.action-btn.primary {
|
||||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.75rem 2rem;
|
padding: 0.75rem 1.5rem;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.retry-btn:hover {
|
/* Контейнер сообщений */
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content */
|
|
||||||
.main-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 1rem 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat Card (новое) */
|
|
||||||
.chat-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-radius: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: calc(100vh - 140px);
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto; /* Центрировать карточку */
|
|
||||||
width: 100%; /* Ширина 100% в контейнере */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Messages Container */
|
|
||||||
.messages-container {
|
.messages-container {
|
||||||
display: flex;
|
flex: 1;
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex-grow: 1;
|
padding: 1rem;
|
||||||
position: relative;
|
|
||||||
height: calc(100% - 80px);
|
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(102, 126, 234, 0.5) rgba(255, 255, 255, 0.1);
|
scrollbar-color: rgba(102, 126, 234, 0.5) rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container::-webkit-scrollbar {
|
.messages-container::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container::-webkit-scrollbar-track {
|
.messages-container::-webkit-scrollbar-track {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container::-webkit-scrollbar-thumb {
|
.messages-container::-webkit-scrollbar-thumb {
|
||||||
@ -977,11 +931,11 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Кнопка прокрутки вниз (новая) */
|
/* Кнопка прокрутки вниз */
|
||||||
.scroll-bottom-btn {
|
.scroll-bottom-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
right: 16px;
|
||||||
bottom: 20px;
|
bottom: 16px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -993,22 +947,15 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-bottom-btn:hover {
|
/* Разделитель даты */
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Date Separator */
|
|
||||||
.date-separator {
|
.date-separator {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 1.5rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-badge {
|
.date-badge {
|
||||||
@ -1016,14 +963,14 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
color: #495057;
|
color: #495057;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message Styles */
|
/* Стили сообщений */
|
||||||
.message-item {
|
.message-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item.sent {
|
.message-item.sent {
|
||||||
@ -1032,11 +979,11 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
|
|
||||||
.message-wrapper {
|
.message-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 75%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
padding: 0.8rem 1rem;
|
padding: 0.7rem 1rem;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@ -1046,30 +993,18 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
color: white;
|
color: white;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.2);
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.received .message-bubble {
|
.received .message-bubble {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: white;
|
||||||
color: #333;
|
color: #333;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
}
|
|
||||||
|
|
||||||
.sent .message-bubble.edited {
|
|
||||||
background: linear-gradient(135deg, #5c70d6, #6742a6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sent .message-bubble.with-actions {
|
|
||||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-content {
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.4rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@ -1086,36 +1021,19 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-time {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-edited {
|
|
||||||
font-style: italic;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-status {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-actions {
|
.message-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
|
left: -60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.4rem;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sent .message-wrapper:hover .message-actions {
|
.sent .message-wrapper:hover .message-actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(-50%) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sent .message-actions {
|
|
||||||
left: -60px; /* Outside message bubble */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-icon {
|
.action-icon {
|
||||||
@ -1128,51 +1046,53 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: white;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-icon:hover {
|
.edit-icon:hover {
|
||||||
background: #0dcaf0;
|
background: #0dcaf0;
|
||||||
color: white;
|
color: white;
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-icon:hover {
|
.delete-icon:hover {
|
||||||
background: #dc3545;
|
background: #dc3545;
|
||||||
color: white;
|
color: white;
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message Input Container */
|
/* Контейнер ввода сообщений */
|
||||||
.message-input-container {
|
.message-input-container {
|
||||||
padding: 1rem;
|
|
||||||
background: white;
|
background: white;
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-form {
|
.message-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-input {
|
.message-input {
|
||||||
flex-grow: 1;
|
flex: 1;
|
||||||
padding: 0.8rem 1rem;
|
padding: 0.7rem 1rem;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
background: white;
|
background: white;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-input:focus {
|
.message-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-actions {
|
.input-actions {
|
||||||
@ -1180,26 +1100,18 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Action Buttons */
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.8rem 1.25rem;
|
padding: 0.7rem 1.2rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
transition: all 0.3s ease;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.primary {
|
|
||||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.secondary {
|
.action-btn.secondary {
|
||||||
background: #f1f3f5;
|
background: #f1f3f5;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
@ -1210,16 +1122,7 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.primary:not(:disabled):hover {
|
/* Контекстное меню */
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.secondary:not(:disabled):hover {
|
|
||||||
background: #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Context Menu */
|
|
||||||
.context-menu {
|
.context-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@ -1230,11 +1133,6 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item {
|
.context-menu-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1245,6 +1143,7 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-item:hover {
|
.context-menu-item:hover {
|
||||||
@ -1263,47 +1162,19 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Адаптивные стили */
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
.chat-card {
|
|
||||||
height: calc(100vh - 120px);
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper {
|
.message-wrapper {
|
||||||
max-width: 85%;
|
max-width: 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-actions {
|
.message-actions {
|
||||||
display: none; /* Hide on mobile, use context menu instead */
|
display: none; /* Скрываем на мобильных, используем контекстное меню */
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container {
|
.action-btn {
|
||||||
padding: 1rem;
|
padding: 0.6rem 1rem;
|
||||||
}
|
font-size: 0.85rem;
|
||||||
|
|
||||||
.chat-input {
|
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-bubble {
|
|
||||||
padding: 0.7rem 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-meta {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-bottom-btn {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
right: 15px;
|
|
||||||
bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participant-name {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
@ -1312,97 +1183,61 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Small phones */
|
|
||||||
@media (max-width: 375px) {
|
@media (max-width: 375px) {
|
||||||
.message-wrapper {
|
.message-wrapper {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input {
|
|
||||||
padding: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-text {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-typing {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu {
|
.context-menu {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chat-info {
|
/* Учитываем наличие нижней навигационной панели */
|
||||||
gap: 0.5rem;
|
@media (max-height: 680px) {
|
||||||
|
.messages-container {
|
||||||
|
padding: 0.8rem;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input-container {
|
||||||
|
padding: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
padding: 0.6rem 1rem;
|
padding: 0.6rem 1rem;
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Large phones and small tablets */
|
/* Ландшафтная ориентация на мобильных */
|
||||||
@media (min-width: 577px) and (max-width: 767px) {
|
@media (max-height: 450px) and (orientation: landscape) {
|
||||||
.chat-card {
|
|
||||||
height: calc(100vh - 130px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper {
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-actions {
|
|
||||||
opacity: 0.7; /* Always visible but slightly transparent */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tablets */
|
|
||||||
@media (min-width: 768px) and (max-width: 991px) {
|
|
||||||
.container {
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-card {
|
|
||||||
height: calc(100vh - 140px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Small desktops */
|
|
||||||
@media (min-width: 992px) and (max-width: 1199px) {
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Landscape orientation on mobile */
|
|
||||||
@media (max-height: 500px) and (orientation: landscape) {
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 1rem;
|
||||||
|
height: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-card {
|
.participant-avatar {
|
||||||
height: calc(100vh - 80px);
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container {
|
.participant-name {
|
||||||
padding: 0.8rem;
|
font-size: 1rem;
|
||||||
height: calc(100% - 60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-container {
|
.message-input-container {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
min-height: 60px;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.message-item {
|
/* Учет нижней мобильной навигации (примерно 60px) */
|
||||||
margin-bottom: 0.4rem;
|
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
||||||
}
|
.message-input-container {
|
||||||
|
padding-bottom: calc(0.8rem + env(safe-area-inset-bottom, 0px));
|
||||||
.date-separator {
|
|
||||||
margin: 0.8rem 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user