Reflex/src/views/admin/AdminConversationDetail.vue
Professional b8369c53c3 фикс
2025-05-26 15:37:26 +07:00

1031 lines
23 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 class="admin-conversation-detail">
<div class="header-controls">
<button @click="goBack" class="back-btn">
<i class="bi-arrow-left"></i>
<span>Назад к списку диалогов</span>
</button>
</div>
<div v-if="loading" class="loading-indicator">
<div class="loading-spinner"></div>
<span>Загрузка диалога...</span>
</div>
<div v-if="error" class="error-message">
<i class="bi-exclamation-triangle"></i>
{{ error }}
</div>
<div v-if="conversation && !loading" class="conversation-container">
<h2>
<i class="bi-chat-text"></i>
<span>Просмотр диалога</span>
</h2>
<div class="conversation-info">
<h3>
<i class="bi-info-circle"></i>
<span>Информация о диалоге</span>
</h3>
<div class="info-grid">
<div class="info-item">
<div class="info-label">ID диалога</div>
<div class="info-value id-value">{{ conversation._id }}</div>
</div>
<div class="info-item">
<div class="info-label">Дата создания</div>
<div class="info-value">{{ formatDate(conversation.createdAt) }}</div>
</div>
<div class="info-item">
<div class="info-label">Последняя активность</div>
<div class="info-value">{{ formatDate(conversation.updatedAt) }}</div>
</div>
</div>
</div>
<div class="participants-section">
<h3>
<i class="bi-people"></i>
<span>Участники диалога</span>
</h3>
<div class="participants-list">
<div
v-for="participant in conversation.participants"
:key="participant._id"
class="participant-card"
>
<div class="participant-photo" v-if="getParticipantPhoto(participant)">
<img :src="getParticipantPhoto(participant)" alt="Фото пользователя">
</div>
<div class="participant-photo placeholder" v-else>
<div class="initials">{{ getInitials(participant.name) }}</div>
</div>
<div class="participant-info">
<div class="participant-name user-name">{{ participant.name }}</div>
<div class="participant-email">{{ participant.email }}</div>
<button @click="viewUser(participant._id)" class="btn user-btn">
<i class="bi-person"></i>
<span>Профиль пользователя</span>
</button>
</div>
</div>
</div>
</div>
<div class="messages-section">
<h3>
<i class="bi-chat-dots"></i>
<span>История сообщений</span>
</h3>
<div v-if="loadingMessages" class="loading-messages">
<div class="loading-spinner small"></div>
<span>Загрузка сообщений...</span>
</div>
<div v-else-if="messages.length === 0" class="no-messages">
<i class="bi-chat-slash"></i>
<p>В диалоге нет сообщений</p>
</div>
<div v-else class="messages-list">
<div
v-for="message in messages"
:key="message._id"
class="message-item"
:class="{ 'sender-1': message.sender._id === conversation.participants[0]._id, 'sender-2': message.sender._id === conversation.participants[1]._id }"
>
<div class="message-header">
<div class="message-sender">{{ message.sender.name }}</div>
<div class="message-time">{{ formatMessageTime(message.createdAt) }}</div>
</div>
<div class="message-content">{{ message.text }}</div>
<div class="message-status">
{{ getMessageStatus(message.status) }}
<span v-if="message.isEdited" class="edited-tag">(отредактировано)</span>
</div>
</div>
</div>
<div v-if="pagination.pages > 1" class="pagination">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage === 1"
class="pagination-btn prev-btn"
>
<i class="bi-chevron-left"></i>
<span>Более новые</span>
</button>
<span class="pagination-info">
{{ currentPage }} из {{ pagination.pages }}
</span>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage === pagination.pages"
class="pagination-btn next-btn"
>
<span>Более старые</span>
<i class="bi-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import axios from 'axios';
export default {
name: 'AdminConversationDetail',
props: {
id: {
type: String,
required: true
}
},
setup(props) {
const router = useRouter();
const route = useRoute();
const conversation = ref(null);
const messages = ref([]);
const loading = ref(false);
const loadingMessages = ref(false);
const error = ref(null);
const currentPage = ref(1);
const pagination = ref({
page: 1,
limit: 50,
total: 0,
pages: 0
});
const loadConversation = async () => {
loading.value = true;
error.value = null;
try {
const token = localStorage.getItem('userToken');
// Прямой запрос к диалогу по его ID
const response = await axios.get(
`/api/admin/conversations/${props.id}`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
if (response.data) {
conversation.value = response.data;
// Загружаем сообщения для диалога
loadMessages();
} else {
error.value = 'Диалог не найден';
}
} catch (err) {
console.error('Ошибка при загрузке данных диалога:', err);
if (err.response && err.response.status === 404) {
error.value = 'Диалог не найден. Проверьте ID диалога.';
} else {
error.value = 'Ошибка при загрузке информации о диалоге. Пожалуйста, попробуйте позже.';
}
} finally {
loading.value = false;
}
};
const loadMessages = async () => {
if (!conversation.value) return;
loadingMessages.value = true;
try {
const token = localStorage.getItem('userToken'); // Исправлено с 'token' на 'userToken'
const response = await axios.get(
`/api/admin/conversations/${conversation.value._id}/messages`,
{
params: {
page: currentPage.value,
limit: pagination.value.limit
},
headers: {
Authorization: `Bearer ${token}`
}
}
);
messages.value = response.data.messages;
pagination.value = response.data.pagination;
} catch (err) {
console.error('Ошибка при загрузке сообщений:', err);
error.value = 'Ошибка при загрузке сообщений. Пожалуйста, попробуйте позже.';
} finally {
loadingMessages.value = false;
}
};
// Переход назад к списку диалогов
const goBack = () => {
router.push({ name: 'AdminConversations' });
};
// Переход к профилю пользователя
const viewUser = (userId) => {
router.push({ name: 'AdminUserDetail', params: { id: userId } });
};
// Получение фотографии участника диалога
const getParticipantPhoto = (participant) => {
if (!participant || !participant.photos || participant.photos.length === 0) return null;
const profilePhoto = participant.photos.find(p => p.isProfilePhoto);
return profilePhoto ? profilePhoto.url : participant.photos[0].url;
};
// Получение инициалов пользователя
const getInitials = (name) => {
if (!name) return '?';
return name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.substring(0, 2);
};
// Форматирование даты
const formatDate = (dateString) => {
if (!dateString) return 'Не указана';
const date = new Date(dateString);
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Форматирование времени сообщения
const formatMessageTime = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Получение статуса сообщения
const getMessageStatus = (status) => {
switch(status) {
case 'sending': return 'Отправляется';
case 'delivered': return 'Доставлено';
case 'read': return 'Прочитано';
default: return status;
}
};
// Изменение страницы пагинации
const changePage = (page) => {
if (page < 1 || page > pagination.value.pages) return;
currentPage.value = page;
loadMessages();
};
onMounted(() => {
loadConversation();
});
return {
conversation,
messages,
loading,
loadingMessages,
error,
currentPage,
pagination,
goBack,
viewUser,
getParticipantPhoto,
getInitials,
formatDate,
formatMessageTime,
getMessageStatus,
changePage
};
}
}
</script>
<style scoped>
.admin-conversation-detail {
padding: 1.5rem;
padding-bottom: 2rem;
}
.header-controls {
display: flex;
justify-content: space-between;
margin-bottom: 1.5rem;
}
.back-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 6px;
cursor: pointer;
color: #333;
font-weight: 500;
transition: all 0.2s ease;
}
.back-btn:hover {
background-color: #e9e9e9;
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1rem;
color: #666;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 62, 104, 0.2);
border-left: 4px solid #ff3e68;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
.loading-spinner.small {
width: 30px;
height: 30px;
border-width: 3px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
padding: 1rem;
background-color: #ffebee;
color: #d32f2f;
border-radius: 8px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.error-message i {
font-size: 1.25rem;
color: #d32f2f;
}
h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #333;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
h2 i {
color: #ff3e68;
}
h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #333;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
h3 i {
color: #666;
}
.conversation-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.conversation-info {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 1.5rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.25rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.info-label {
font-size: 0.9rem;
color: #666;
font-weight: 500;
}
.info-value {
font-weight: 500;
color: #333;
}
.id-value {
font-family: monospace;
word-break: break-all;
}
.participants-section {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 1.5rem;
overflow-x: hidden; /* Добавляем скрытие горизонтального переполнения */
}
.participants-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
max-width: 100%;
overflow: hidden; /* Скрываем переполнение */
}
.participant-card {
display: flex;
align-items: center;
gap: 1.25rem;
padding: 1.25rem;
border: 1px solid #eee;
border-radius: 10px;
background-color: #f9f9f9;
transition: all 0.2s ease;
max-width: 100%; /* Ограничиваем ширину */
overflow: hidden; /* Скрываем переполнение */
}
.participant-photo {
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.participant-info {
flex: 1;
min-width: 0; /* Важно для текста с переполнением */
width: 100%; /* Занимает всё доступное пространство */
overflow: hidden; /* Скрываем переполнение */
}
.participant-name {
font-weight: 600;
margin-bottom: 0.35rem;
font-size: 1.1rem;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%; /* Изменяем с calc(100% - 20px) на 100% */
}
.participant-email {
font-size: 0.9rem;
color: #666;
margin-bottom: 0.85rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%; /* Добавляем максимальную ширину */
}
.btn {
padding: 0.5rem 0.85rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease;
max-width: 100%; /* Ограничиваем ширину кнопки */
overflow: hidden; /* Скрываем переполнение */
}
.user-btn {
background-color: rgba(25, 118, 210, 0.1);
color: #1976d2;
width: 100%; /* Кнопка занимает всю доступную ширину */
}
.user-btn:hover {
background-color: rgba(25, 118, 210, 0.15);
transform: translateY(-2px);
}
.messages-section {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 1.5rem;
}
.loading-messages {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
color: #666;
}
.no-messages {
text-align: center;
padding: 3rem 1rem;
color: #666;
}
.no-messages i {
font-size: 3rem;
color: #ddd;
margin-bottom: 1rem;
}
.no-messages p {
font-size: 1.1rem;
margin: 0;
}
.messages-list {
display: flex;
flex-direction: column;
gap: 1.25rem;
padding: 0.5rem 0;
}
.message-item {
padding: 1rem 1.25rem;
border-radius: 12px;
max-width: 80%;
box-shadow: 0 2px 5px rgba(0,0,0,0.06);
transition: all 0.2s ease;
}
.message-item:hover {
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.message-item.sender-1 {
background-color: #e3f2fd;
align-self: flex-start;
border-bottom-left-radius: 4px;
}
.message-item.sender-2 {
background-color: #f8bbd0;
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.6rem;
font-size: 0.9rem;
}
.message-sender {
font-weight: 600;
color: #495057;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
display: inline-block;
}
.message-time {
color: #666;
}
.message-content {
margin-bottom: 0.6rem;
white-space: pre-wrap;
line-height: 1.4;
}
.message-status {
text-align: right;
font-size: 0.8rem;
color: #666;
}
.edited-tag {
margin-left: 0.5rem;
font-style: italic;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1.25rem;
margin-top: 1.5rem;
}
.pagination-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.25rem;
background-color: white;
border: 1px solid #e9ecef;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
color: #495057;
font-weight: 500;
}
.pagination-btn:hover:not(:disabled) {
background-color: #f8f9fa;
border-color: #ced4da;
transform: translateY(-2px);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.pagination-info {
color: #6c757d;
font-weight: 500;
}
/* Адаптивные стили для больших экранов (1200px+) */
@media (min-width: 1200px) {
.admin-conversation-detail {
padding: 2rem;
}
h2 {
font-size: 1.75rem;
}
.conversation-container {
gap: 2.5rem;
}
}
/* Адаптивные стили для средних экранов (992px - 1199px) */
@media (min-width: 992px) and (max-width: 1199px) {
.admin-conversation-detail {
padding: 1.5rem;
}
.conversation-container {
gap: 1.75rem;
}
}
/* Адаптивные стили для планшетов (768px - 991px) */
@media (min-width: 768px) and (max-width: 991px) {
.admin-conversation-detail {
padding: 1.25rem;
padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
}
.info-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.participants-list {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.conversation-container {
gap: 1.5rem;
}
}
/* Адаптивные стили для малых планшетов (576px - 767px) */
@media (min-width: 576px) and (max-width: 767px) {
.admin-conversation-detail {
padding: 1rem;
/* Учитываем отступ внизу для нижней навигации на мобильных */
padding-bottom: calc(1rem + 56px + env(safe-area-inset-bottom, 0px));
}
.conversation-container {
gap: 1.25rem;
}
.conversation-info,
.participants-section,
.messages-section {
padding: 1.25rem;
}
.message-item {
max-width: 85%;
}
.participants-list {
grid-template-columns: 1fr;
gap: 1rem;
}
.pagination {
flex-wrap: wrap;
gap: 1rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
}
/* Адаптивные стили для мобильных устройств (до 575px) */
@media (max-width: 575px) {
.admin-conversation-detail {
padding: 0.75rem;
/* Учитываем отступ внизу для нижней навигации на мобильных */
padding-bottom: calc(0.75rem + 56px + env(safe-area-inset-bottom, 0px));
}
.conversation-container {
gap: 1rem;
}
h2 {
font-size: 1.3rem;
margin-bottom: 1rem;
}
h3 {
font-size: 1.1rem;
margin-bottom: 0.75rem;
}
.conversation-info,
.participants-section,
.messages-section {
padding: 1rem;
border-radius: 10px;
}
.info-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.participants-list {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.participant-card {
padding: 1rem;
gap: 1rem;
}
.participant-photo {
width: 50px;
height: 50px;
}
.message-item {
max-width: 90%;
padding: 0.75rem 1rem;
}
.message-header {
flex-direction: column;
gap: 0.25rem;
margin-bottom: 0.5rem;
width: 100%;
}
.message-sender {
max-width: 100%;
}
.pagination {
flex-direction: column;
gap: 0.75rem;
align-items: stretch;
}
.pagination-btn {
width: 100%;
justify-content: center;
padding: 0.6rem 1rem;
}
.pagination-info {
text-align: center;
font-size: 0.9rem;
}
.back-btn {
width: 100%;
justify-content: center;
}
.back-btn span {
display: none;
}
.back-btn i {
font-size: 1.2rem;
}
.header-controls {
margin-bottom: 1rem;
}
}
/* Специальные стили для очень маленьких экранов */
@media (max-width: 360px) {
.admin-conversation-detail {
padding: 0.5rem;
padding-bottom: calc(0.5rem + 56px + env(safe-area-inset-bottom, 0px));
}
.conversation-info,
.participants-section,
.messages-section {
padding: 0.75rem;
border-radius: 8px;
}
.message-header {
align-items: flex-start;
width: 100%;
}
.message-time {
font-size: 0.7rem;
}
.message-sender {
font-size: 0.8rem;
}
.message-content {
font-size: 0.9rem;
}
.user-btn {
width: 100%;
padding: 0.5rem;
font-size: 0.8rem;
justify-content: center;
}
.participant-name {
font-size: 1rem;
}
.participant-email {
font-size: 0.8rem;
}
.info-label {
font-size: 0.8rem;
}
.info-value {
font-size: 0.9rem;
}
}
/* Ландшафтная ориентация на мобильных */
@media (max-height: 450px) and (orientation: landscape) {
.admin-conversation-detail {
padding: 0.75rem;
padding-bottom: calc(0.75rem + 50px + env(safe-area-inset-bottom, 0px));
}
.conversation-container {
gap: 0.75rem;
}
.header-controls {
margin-bottom: 0.75rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 0.75rem;
}
h3 {
font-size: 1rem;
margin-bottom: 0.5rem;
}
.conversation-info,
.participants-section,
.messages-section {
padding: 0.75rem;
}
.pagination {
margin-top: 0.75rem;
flex-direction: row;
align-items: center;
}
.pagination-btn {
width: auto;
}
/* Горизонтальное расположение участников диалога */
.participants-list {
display: flex;
overflow-x: auto;
gap: 1rem;
padding: 0.5rem 0;
}
.participant-card {
min-width: 280px;
width: auto;
}
/* Оптимизация сообщений для ландшафтной ориентации */
.messages-list {
max-height: 200px;
overflow-y: auto;
padding: 0.5rem;
}
}
/* Добавляем анимацию для плавного отображения элементов */
.message-item,
.participant-card,
.conversation-info,
.participants-section,
.messages-section {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>