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

657 lines
15 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-conversations">
<h2>Управление диалогами</h2>
<div class="search-bar">
<input
type="text"
v-model="searchUserId"
placeholder="Поиск диалогов по ID пользователя..."
@input="debouncedSearch"
/>
</div>
<div v-if="loading" class="loading-indicator">
<div class="spinner"></div>
<p>Загрузка диалогов...</p>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<!-- Десктопная таблица (отображается только на больших экранах) -->
<div v-if="!loading && !error" class="desktop-table">
<div class="conversations-table-container">
<table class="conversations-table">
<thead>
<tr>
<th>ID диалога</th>
<th>Участники</th>
<th>Последнее сообщение</th>
<th>Дата создания</th>
<th>Последняя активность</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="conversation in conversations" :key="conversation._id">
<td>{{ shortenId(conversation._id) }}</td>
<td class="participants-cell">
<div v-for="participant in conversation.participants" :key="participant._id" class="participant">
{{ participant.name }} ({{ participant.email }})
</div>
</td>
<td>
{{ conversation.lastMessage ? truncateText(conversation.lastMessage.text, 30) : 'Нет сообщений' }}
</td>
<td>{{ formatDate(conversation.createdAt) }}</td>
<td>{{ formatDate(conversation.updatedAt) }}</td>
<td class="actions">
<button @click="viewConversation(conversation._id)" class="btn view-btn">
Просмотр
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Мобильные карточки (отображаются только на малых экранах) -->
<div v-if="!loading && !error" class="mobile-cards">
<div
v-for="conversation in conversations"
:key="conversation._id"
class="conversation-card"
@click="viewConversation(conversation._id)"
>
<div class="card-header">
<div class="card-id">ID: {{ shortenId(conversation._id) }}</div>
<div class="card-date">{{ formatDate(conversation.updatedAt) }}</div>
</div>
<div class="card-content">
<div class="card-section">
<div class="section-title">Участники:</div>
<div class="section-content">
<div v-for="participant in conversation.participants" :key="participant._id" class="participant-item">
{{ participant.name }}
</div>
</div>
</div>
<div class="card-section">
<div class="section-title">Последнее сообщение:</div>
<div class="section-content message-preview">
{{ conversation.lastMessage ? truncateText(conversation.lastMessage.text, 50) : 'Нет сообщений' }}
</div>
</div>
</div>
<div class="card-footer">
<span>Просмотр диалога</span>
<i class="bi-arrow-right"></i>
</div>
</div>
</div>
<div v-if="!loading && !error && conversations.length === 0" class="no-results">
<i class="bi-chat-dots"></i>
<h3>Диалоги не найдены</h3>
<p>Попробуйте изменить параметры поиска</p>
</div>
<div v-if="!loading && !error && pagination.pages > 1" class="pagination">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage === 1"
class="pagination-btn"
>
&laquo; Назад
</button>
<span class="pagination-info">
Страница {{ currentPage }} из {{ pagination.pages }}
</span>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage === pagination.pages"
class="pagination-btn"
>
Вперёд &raquo;
</button>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
export default {
name: 'AdminConversations',
setup() {
const router = useRouter();
const conversations = ref([]);
const loading = ref(false);
const error = ref(null);
const searchUserId = ref('');
const currentPage = ref(1);
const pagination = ref({
page: 1,
limit: 20,
total: 0,
pages: 0
});
// Таймер для debounce поиска
let searchTimer = null;
const debouncedSearch = () => {
// Сбрасываем предыдущий таймер
if (searchTimer) {
clearTimeout(searchTimer);
}
// Устанавливаем новый таймер (задержка 500мс)
searchTimer = setTimeout(() => {
currentPage.value = 1; // При новом поиске сбрасываем на первую страницу
loadConversations();
}, 500);
};
const loadConversations = async () => {
loading.value = true;
error.value = null;
try {
// Определяем параметры запроса
const params = {
page: currentPage.value,
limit: pagination.value.limit
};
// Добавляем поиск по ID пользователя, если указан
if (searchUserId.value) {
params.userId = searchUserId.value;
}
const token = localStorage.getItem('userToken'); // Исправлено с 'token' на 'userToken'
const response = await axios.get(`/api/admin/conversations`, {
params,
headers: {
Authorization: `Bearer ${token}`
}
});
conversations.value = response.data.conversations;
pagination.value = response.data.pagination;
} catch (err) {
console.error('Ошибка при загрузке диалогов:', err);
error.value = 'Ошибка при загрузке списка диалогов. Пожалуйста, попробуйте позже.';
} finally {
loading.value = false;
}
};
// Функция для сокращения ID (показывать только первые 6 символов)
const shortenId = (id) => {
return id ? id.substring(0, 6) + '...' : '';
};
// Форматирование даты
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 truncateText = (text, maxLength) => {
if (!text) return '';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
// Переход на страницу детальной информации о диалоге
const viewConversation = (conversationId) => {
router.push({ name: 'AdminConversationDetail', params: { id: conversationId } });
};
// Изменение страницы пагинации
const changePage = (page) => {
if (page < 1 || page > pagination.value.pages) return;
currentPage.value = page;
loadConversations();
};
onMounted(() => {
loadConversations();
});
return {
conversations,
loading,
error,
searchUserId,
currentPage,
pagination,
loadConversations,
debouncedSearch, // Добавляем новую функцию в return
shortenId,
formatDate,
truncateText,
viewConversation,
changePage
};
}
}
</script>
<style scoped>
.admin-conversations {
padding: 1.5rem;
padding-bottom: 2rem;
}
h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #333;
font-weight: 600;
position: relative;
padding-left: 0.5rem;
display: inline-block;
}
h2::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 3px;
background: linear-gradient(to bottom, #ff3e68, #ff5252);
border-radius: 3px;
}
.search-bar {
display: flex;
margin-bottom: 1.5rem;
}
.search-bar input {
flex: 1;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
color: #666;
}
.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;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
padding: 1rem;
background-color: #ffebee;
color: #d32f2f;
border-radius: 4px;
margin-bottom: 1rem;
}
/* Стили для десктопной таблицы */
.conversations-table-container {
overflow-x: auto;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.conversations-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
.conversations-table th, .conversations-table td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.conversations-table th {
background-color: #f5f5f5;
font-weight: 500;
color: #444;
}
.conversations-table tr:hover {
background-color: #f9f9f9;
}
.participants-cell {
max-width: 300px;
}
.participant {
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: block;
}
.participant:last-child {
margin-bottom: 0;
}
.actions {
white-space: nowrap;
}
.btn {
padding: 0.4rem 0.75rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 0.875rem;
}
.view-btn {
background-color: #e3f2fd;
color: #1976d2;
}
.view-btn:hover {
background-color: #bbdefb;
}
/* Стили для мобильных карточек */
.mobile-cards {
display: none;
flex-direction: column;
gap: 1rem;
}
.conversation-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.conversation-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
cursor: pointer;
}
.card-header {
display: flex;
justify-content: space-between;
padding: 0.75rem;
background: #f8f9fa;
border-bottom: 1px solid #eee;
}
.card-id {
font-weight: 500;
color: #1976d2;
}
.card-date {
font-size: 0.85rem;
color: #666;
}
.card-content {
padding: 0.75rem;
}
.card-section {
margin-bottom: 0.75rem;
}
.card-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 0.85rem;
color: #666;
margin-bottom: 0.25rem;
}
.section-content {
color: #333;
}
.participant-item {
font-size: 0.9rem;
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.message-preview {
font-style: italic;
color: #495057;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background-color: #f8f9fa;
border-top: 1px solid #eee;
font-weight: 500;
color: #1976d2;
}
.card-footer i {
transition: transform 0.2s;
}
.conversation-card:hover .card-footer i {
transform: translateX(2px);
}
/* Стили для пустого результата */
.no-results {
text-align: center;
padding: 2rem;
color: #666;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.no-results i {
font-size: 3rem;
color: #ccc;
margin-bottom: 1rem;
}
.no-results h3 {
margin: 0 0 0.5rem;
color: #333;
}
.no-results p {
margin: 0;
color: #666;
}
/* Стили для пагинации */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 1.5rem;
}
.pagination-btn {
padding: 0.5rem 1rem;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.pagination-btn:hover:not(:disabled) {
background-color: #e9e9e9;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
color: #666;
}
/* Адаптивные стили */
@media (max-width: 1199px) {
.admin-conversations {
padding: 1.25rem;
}
}
@media (max-width: 991px) {
/* На планшетах и меньше */
.desktop-table {
display: none;
}
.mobile-cards {
display: flex;
}
.admin-conversations {
padding: 1rem;
/* Добавляем отступ для нижней навигации */
padding-bottom: calc(1rem + 56px + env(safe-area-inset-bottom, 0px));
}
}
@media (min-width: 992px) {
/* На десктопах */
.desktop-table {
display: block;
}
.mobile-cards {
display: none;
}
}
@media (max-width: 767px) {
/* Мобильные устройства */
h2 {
font-size: 1.3rem;
}
.search-bar {
flex-direction: column;
gap: 0.5rem;
}
.pagination {
flex-wrap: wrap;
gap: 0.5rem;
}
}
@media (max-width: 480px) {
/* Маленькие мобильные устройства */
.admin-conversations {
padding: 0.75rem;
padding-bottom: calc(0.75rem + 56px + env(safe-area-inset-bottom, 0px));
}
h2 {
font-size: 1.2rem;
margin-bottom: 1rem;
}
.search-bar input {
padding: 0.6rem;
}
.card-header {
flex-direction: column;
gap: 0.25rem;
}
.card-date {
font-size: 0.8rem;
}
.participant-item {
font-size: 0.85rem;
}
.pagination-btn {
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
}
}
/* Очень маленькие экраны */
@media (max-width: 360px) {
.admin-conversations {
padding: 0.5rem;
padding-bottom: calc(0.5rem + 56px + env(safe-area-inset-bottom, 0px));
}
.card-content {
padding: 0.6rem;
}
}
/* Ландшафтная ориентация на мобильных */
@media (max-height: 450px) and (orientation: landscape) {
.admin-conversations {
padding-bottom: calc(1rem + 50px + env(safe-area-inset-bottom, 0px));
}
}
</style>