фикс стилей

This commit is contained in:
Professional 2025-05-26 13:43:48 +07:00
parent c2791255ee
commit 04330f6c2d
4 changed files with 1259 additions and 368 deletions

View File

@ -562,4 +562,183 @@ h3 {
.pagination-info {
color: #666;
}
/* Адаптивные стили для больших экранов (1200px+) */
@media (min-width: 1200px) {
.admin-conversation-detail {
padding: 2rem;
}
h2 {
font-size: 1.75rem;
}
}
/* Адаптивные стили для средних экранов (992px - 1199px) */
@media (min-width: 992px) and (max-width: 1199px) {
.admin-conversation-detail {
padding: 1.5rem;
}
}
/* Адаптивные стили для планшетов (768px - 991px) */
@media (min-width: 768px) and (max-width: 991px) {
.admin-conversation-detail {
padding: 1.25rem;
}
.info-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.participants-list {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
/* Адаптивные стили для малых планшетов (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));
}
.message-item {
max-width: 85%;
}
.participants-list {
grid-template-columns: 1fr;
}
.pagination {
flex-wrap: wrap;
}
}
/* Адаптивные стили для мобильных устройств (до 575px) */
@media (max-width: 575px) {
.admin-conversation-detail {
padding: 0.75rem;
/* Учитываем отступ внизу для нижней навигации на мобильных */
padding-bottom: calc(0.75rem + 56px + env(safe-area-inset-bottom, 0px));
}
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;
}
.info-grid {
grid-template-columns: 1fr;
}
.participants-list {
grid-template-columns: 1fr;
}
.participant-card {
padding: 0.75rem;
}
.participant-photo {
width: 50px;
height: 50px;
}
.message-item {
max-width: 90%;
padding: 0.75rem;
}
.pagination {
flex-direction: column;
gap: 0.5rem;
align-items: stretch;
}
.pagination-btn {
width: 100%;
text-align: center;
}
.pagination-info {
text-align: center;
}
}
/* Специальные стили для очень маленьких экранов */
@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;
}
.message-time {
font-size: 0.7rem;
}
.message-sender {
font-size: 0.8rem;
}
.user-btn {
padding: 0.35rem 0.5rem;
font-size: 0.8rem;
}
}
/* Ландшафтная ориентация на мобильных */
@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: 1rem;
}
.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;
}
}
</style>

View File

@ -12,72 +12,113 @@
</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="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 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>
</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 v-if="conversations.length === 0" class="no-results">
Диалоги не найдены
</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="pagination.pages > 1" class="pagination">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage === 1"
class="pagination-btn"
>
&laquo; Назад
</button>
<div v-if="!loading && !error && conversations.length === 0" class="no-results">
<i class="bi-chat-dots"></i>
<h3>Диалоги не найдены</h3>
<p>Попробуйте изменить параметры поиска</p>
</div>
<span class="pagination-info">
Страница {{ currentPage }} из {{ pagination.pages }}
</span>
<div v-if="!loading && !error && pagination.pages > 1" class="pagination">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage === 1"
class="pagination-btn"
>
&laquo; Назад
</button>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage === pagination.pages"
class="pagination-btn"
>
Вперёд &raquo;
</button>
</div>
<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>
@ -202,6 +243,7 @@ export default {
<style scoped>
.admin-conversations {
padding: 1.5rem;
padding-bottom: 2rem;
}
@ -209,6 +251,21 @@ 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 {
@ -234,12 +291,35 @@ h2 {
font-weight: 500;
}
.search-btn:hover {
background-color: #e9365e;
}
.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;
@ -248,8 +328,11 @@ h2 {
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 {
@ -270,6 +353,10 @@ h2 {
color: #444;
}
.conversations-table tr:hover {
background-color: #f9f9f9;
}
.participants-cell {
max-width: 300px;
}
@ -300,12 +387,127 @@ h2 {
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;
}
.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;
@ -320,6 +522,11 @@ h2 {
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 {
@ -330,4 +537,115 @@ h2 {
.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;
}
.search-btn {
width: 100%;
padding: 0.75rem;
}
.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>

View File

@ -3,10 +3,12 @@
<h2>Статистика приложения</h2>
<div v-if="loading" class="loading-indicator">
Загрузка статистики...
<div class="loading-spinner"></div>
<div>Загрузка статистики...</div>
</div>
<div v-if="error" class="error-message">
<i class="bi-exclamation-triangle"></i>
{{ error }}
</div>
@ -15,36 +17,34 @@
<h3>Пользователи</h3>
<div class="stat-cards">
<div class="stat-card">
<div class="stat-card users">
<div class="stat-value">{{ statistics.users.total }}</div>
<div class="stat-label">Всего пользователей</div>
</div>
<div class="stat-card">
<div class="stat-card active">
<div class="stat-value">{{ statistics.users.active }}</div>
<div class="stat-label">Активных пользователей</div>
<div class="stat-label">Активных</div>
</div>
<div class="stat-card">
<div class="stat-card blocked">
<div class="stat-value">{{ statistics.users.inactive }}</div>
<div class="stat-label">Заблокированных</div>
</div>
<div class="stat-card">
<div class="stat-card new">
<div class="stat-value">{{ statistics.users.newIn30Days }}</div>
<div class="stat-label">Новых за 30 дней</div>
</div>
</div>
<div class="gender-distribution">
<h4>Распределение по полу</h4>
<h4>Распределение пользователей по полу</h4>
<div class="gender-chart">
<div class="gender-bar">
<div
class="gender-male"
:style="{
width: calculateGenderPercentage('male') + '%'
}"
:style="{ width: calculateGenderPercentage('male') + '%' }"
>
<span v-if="calculateGenderPercentage('male') > 10">
{{ statistics.users.genderDistribution.male }}
@ -52,9 +52,7 @@
</div>
<div
class="gender-female"
:style="{
width: calculateGenderPercentage('female') + '%'
}"
:style="{ width: calculateGenderPercentage('female') + '%' }"
>
<span v-if="calculateGenderPercentage('female') > 10">
{{ statistics.users.genderDistribution.female }}
@ -62,9 +60,7 @@
</div>
<div
class="gender-other"
:style="{
width: calculateGenderPercentage('other') + '%'
}"
:style="{ width: calculateGenderPercentage('other') + '%' }"
>
<span v-if="calculateGenderPercentage('other') > 10">
{{ statistics.users.genderDistribution.other }}
@ -93,31 +89,33 @@
<h3>Активность пользователей</h3>
<div class="stat-cards">
<div class="stat-card">
<div class="stat-card messages">
<div class="stat-value">{{ statistics.activity.totalMessages }}</div>
<div class="stat-label">Всего сообщений</div>
</div>
<div class="stat-card">
<div class="stat-card conversations">
<div class="stat-value">{{ statistics.activity.totalConversations }}</div>
<div class="stat-label">Всего диалогов</div>
</div>
<div class="stat-card">
<div class="stat-card views">
<div class="stat-value">{{ statistics.activity.totalProfileViews }}</div>
<div class="stat-label">Просмотров профилей</div>
</div>
</div>
<h4>Средняя активность</h4>
<div class="average-stats">
<div class="average-item">
<div class="average-label">Сообщений на пользователя:</div>
<div class="average-value">{{ statistics.activity.averages.messagesPerUser }}</div>
</div>
<div class="average-item">
<div class="average-label">Диалогов на пользователя:</div>
<div class="average-value">{{ statistics.activity.averages.conversationsPerUser }}</div>
<div class="average-stats-container">
<h4>Средняя активность</h4>
<div class="average-stats">
<div class="average-item">
<div class="average-label">Сообщений на пользователя:</div>
<div class="average-value">{{ statistics.activity.averages.messagesPerUser }}</div>
</div>
<div class="average-item">
<div class="average-label">Диалогов на пользователя:</div>
<div class="average-value">{{ statistics.activity.averages.conversationsPerUser }}</div>
</div>
</div>
</div>
</div>
@ -189,6 +187,7 @@ export default {
<style scoped>
.admin-statistics {
padding: 1.5rem;
padding-bottom: 2rem;
}
@ -213,12 +212,49 @@ h2::before {
border-radius: 3px;
}
h3 {
margin-top: 0;
margin-bottom: 1.25rem;
color: #333;
font-weight: 600;
font-size: 1.3rem;
}
h4 {
margin: 1.5rem 0 1rem 0;
color: #333;
font-weight: 500;
font-size: 1.1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
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;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
padding: 1rem;
background-color: #ffebee;
@ -226,6 +262,14 @@ h2::before {
color: #d32f2f;
margin-bottom: 1.5rem;
border-radius: 4px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.error-message i {
color: #d32f2f;
font-size: 1.2rem;
}
.statistics-container {
@ -234,40 +278,67 @@ h2::before {
gap: 2rem;
}
.chart-container {
.stat-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 1.5rem;
overflow: hidden;
border: 1px solid #f1f3f5;
}
.section-title {
margin-top: 0;
margin-bottom: 1rem;
font-weight: 600;
color: #333;
}
.stats-section {
border-radius: 8px;
margin-bottom: 2rem;
}
.stats-cards {
.stat-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.stat-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
background-color: #f8f9fa;
border-radius: 10px;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
border-top: 3px solid transparent;
border: 1px solid #f1f3f5;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, transparent, #ff3e68, transparent);
}
.stat-card.users::before {
background: linear-gradient(to right, #ff3e68, #ff5252);
}
.stat-card.active::before {
background: linear-gradient(to right, #4caf50, #8bc34a);
}
.stat-card.blocked::before {
background: linear-gradient(to right, #f44336, #e91e63);
}
.stat-card.new::before {
background: linear-gradient(to right, #ff9800, #ffc107);
}
.stat-card.messages::before {
background: linear-gradient(to right, #2196f3, #03a9f4);
}
.stat-card.conversations::before {
background: linear-gradient(to right, #9c27b0, #673ab7);
}
.stat-card.views::before {
background: linear-gradient(to right, #009688, #4caf50);
}
.stat-card:hover {
@ -275,22 +346,6 @@ h2::before {
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.stat-card.users {
border-color: #ff3e68;
}
.stat-card.messages {
border-color: #4caf50;
}
.stat-card.conversations {
border-color: #2196f3;
}
.stat-card.views {
border-color: #ff9800;
}
.stat-value {
font-size: 2.2rem;
font-weight: bold;
@ -302,48 +357,45 @@ h2::before {
color: #ff3e68;
}
.stat-card.messages .stat-value {
.stat-card.active .stat-value {
color: #4caf50;
}
.stat-card.conversations .stat-value {
.stat-card.blocked .stat-value {
color: #f44336;
}
.stat-card.new .stat-value {
color: #ff9800;
}
.stat-card.messages .stat-value {
color: #2196f3;
}
.stat-card.conversations .stat-value {
color: #9c27b0;
}
.stat-card.views .stat-value {
color: #ff9800;
color: #009688;
}
.stat-label {
color: #777;
font-size: 0.9rem;
font-size: 0.95rem;
font-weight: 500;
}
/* Стили для блока распределения по полу */
.gender-distribution {
margin-top: 2rem;
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
background-color: #f8f9fa;
border-radius: 10px;
padding: 1.5rem;
border: 1px solid #f1f3f5;
}
.gender-distribution h4 {
margin: 0 0 1.5rem 0;
color: #333;
font-weight: 600;
font-size: 1.1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.gender-distribution h4::before {
content: '👥';
font-size: 1.2rem;
}
.gender-chart {
display: flex;
flex-direction: column;
@ -356,7 +408,7 @@ h2::before {
height: 40px;
border-radius: 20px;
overflow: hidden;
background-color: #f8f9fa;
background-color: #f1f3f5;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);
position: relative;
}
@ -369,8 +421,8 @@ h2::before {
justify-content: center;
color: white;
font-weight: 600;
font-size: 0.85rem;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
font-size: 0.9rem;
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
transition: all 0.3s ease;
min-width: 0;
position: relative;
@ -408,7 +460,7 @@ h2::before {
.gender-legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
gap: 0.75rem;
margin-top: 1rem;
justify-content: center;
}
@ -418,16 +470,17 @@ h2::before {
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa;
background-color: white;
border-radius: 8px;
transition: all 0.2s ease;
border: 1px solid transparent;
border: 1px solid #f1f3f5;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.legend-item:hover {
background-color: white;
border-color: #e9ecef;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transform: translateY(-1px);
}
@ -458,82 +511,108 @@ h2::before {
white-space: nowrap;
}
.averages-section {
/* Блок средних значений */
.average-stats-container {
margin-top: 1.5rem;
background-color: #f8f9fa;
border-radius: 10px;
padding: 1.5rem;
border: 1px solid #f1f3f5;
}
.averages-title {
margin-top: 0;
margin-bottom: 1rem;
font-weight: 500;
color: #333;
font-size: 1.1rem;
}
.averages-container {
display: flex;
flex-wrap: wrap;
.average-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.average-item {
background-color: #f8f9fa;
padding: 0.8rem 1.2rem;
border-radius: 8px;
flex: 1;
min-width: 200px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: white;
padding: 1rem 1.25rem;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
border: 1px solid #f1f3f5;
transition: all 0.2s ease;
}
.average-item:hover {
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0,0,0,0.08);
}
.average-label {
color: #555;
font-weight: 500;
font-size: 0.95rem;
}
.average-value {
font-weight: bold;
color: #ff3e68;
}
.donut-chart {
max-width: 300px;
margin: 0 auto;
}
.subcards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.5rem;
margin-top: 1rem;
}
.subcard {
background-color: #f8f9fa;
padding: 0.8rem;
border-radius: 6px;
text-align: center;
}
.subcard-value {
font-weight: bold;
color: #ff3e68;
font-size: 1.2rem;
}
.subcard-label {
font-size: 0.75rem;
color: #777;
margin-top: 0.25rem;
/* Адаптивное отображение */
@media (max-width: 1199px) {
.admin-statistics {
padding: 1.25rem;
}
.stat-cards {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
}
@media (max-width: 991px) {
.admin-statistics {
padding: 1rem;
/* Добавляем отступ для нижней навигации */
padding-bottom: calc(1rem + 56px + env(safe-area-inset-bottom, 0px));
}
.stat-card {
padding: 1.25rem;
}
.stat-cards {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.stat-value {
font-size: 2rem;
}
.gender-distribution,
.average-stats-container {
padding: 1.25rem;
}
.average-stats {
grid-template-columns: 1fr;
}
}
/* Адаптивное отображение */
@media (max-width: 767px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
gap: 0.8rem;
}
.stat-value {
font-size: 1.8rem;
}
.stat-label {
font-size: 0.85rem;
}
.stat-section {
padding: 1.25rem;
}
.gender-bar {
height: 35px;
}
@ -544,25 +623,38 @@ h2::before {
font-size: 0.8rem;
}
.gender-distribution {
padding: 1rem;
.gender-distribution,
.average-stats-container {
padding: 1.25rem;
margin-top: 1.5rem;
}
.legend-label {
font-size: 0.85rem;
.legend-item {
padding: 0.4rem 0.6rem;
flex: 1;
justify-content: center;
}
h2 {
font-size: 1.4rem;
font-size: 1.3rem;
margin-bottom: 1.2rem;
}
.section-title {
font-size: 1.1rem;
h3 {
font-size: 1.2rem;
margin-bottom: 1rem;
}
.stats-cards {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
h4 {
font-size: 1rem;
margin: 0.75rem 0;
}
.average-item {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem 1rem;
}
.admin-statistics {
@ -572,20 +664,31 @@ h2::before {
@media (max-width: 576px) {
.stat-card {
padding: 1.2rem;
padding: 1rem;
}
.stat-value {
font-size: 1.6rem;
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.stats-cards {
grid-template-columns: repeat(2, 1fr);
.stat-label {
font-size: 0.8rem;
}
.gender-distribution {
.admin-statistics {
padding: 0.75rem;
padding-bottom: calc(0.75rem + 56px + env(safe-area-inset-bottom, 0px));
}
.stat-section {
padding: 1rem;
margin-top: 1.5rem;
}
.gender-distribution,
.average-stats-container {
padding: 1rem;
margin-top: 1rem;
}
.gender-bar {
@ -600,17 +703,72 @@ h2::before {
.gender-legend {
flex-direction: column;
gap: 0.75rem;
align-items: center;
gap: 0.5rem;
}
.legend-item {
justify-content: center;
min-width: 120px;
width: 100%;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1rem;
}
h3 {
font-size: 1.1rem;
margin-bottom: 0.8rem;
}
h4 {
font-size: 0.95rem;
margin: 0.5rem 0;
}
}
@media (max-width: 480px) {
.stat-cards {
gap: 0.6rem;
}
.stat-card {
border-radius: 8px;
padding: 0.8rem;
}
.stat-value {
font-size: 1.3rem;
margin-bottom: 0.2rem;
}
.stat-label {
font-size: 0.75rem;
}
.average-label {
font-size: 0.85rem;
}
.average-value {
font-size: 1rem;
}
}
@media (max-width: 360px) {
.admin-statistics {
padding: 0.5rem;
padding-bottom: calc(0.5rem + 56px + env(safe-area-inset-bottom, 0px));
}
.stat-section,
.gender-distribution,
.average-stats-container {
padding: 0.75rem;
border-radius: 8px;
}
.gender-bar {
height: 25px;
}
}
@ -626,5 +784,29 @@ h2::before {
.admin-statistics {
padding-bottom: calc(50px + env(safe-area-inset-bottom, 0px));
}
.stat-cards {
grid-template-columns: repeat(4, 1fr);
}
.stat-card {
padding: 0.75rem;
}
.stat-value {
font-size: 1.5rem;
}
.stat-label {
font-size: 0.75rem;
}
.gender-distribution {
margin-top: 1rem;
}
.gender-legend {
flex-direction: row;
}
}
</style>

View File

@ -26,80 +26,137 @@
</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="!loading && !error" class="users-table-container">
<table class="users-table">
<thead>
<tr>
<th>ID</th>
<th>Имя</th>
<th>Email</th>
<th class="hide-sm">Дата регистрации</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user._id">
<td>{{ shortenId(user._id) }}</td>
<td>{{ user.name }}</td>
<td class="email-cell">{{ user.email }}</td>
<td class="hide-sm">{{ formatDate(user.createdAt) }}</td>
<td>
<span class="status-badge" :class="{ 'active': user.isActive, 'blocked': !user.isActive }">
{{ user.isActive ? 'Активен' : 'Заблокирован' }}
</span>
</td>
<td class="actions">
<button @click="viewUser(user._id)" class="btn view-btn">
<span class="btn-text">Просмотр</span>
<i class="bi-eye"></i>
</button>
<button
@click="toggleUserStatus(user._id, user.isActive)"
class="btn"
:class="user.isActive ? 'block-btn' : 'unblock-btn'"
>
<span class="btn-text">{{ user.isActive ? 'Заблокировать' : 'Разблокировать' }}</span>
<i :class="user.isActive ? 'bi-lock-fill' : 'bi-unlock-fill'"></i>
</button>
</td>
</tr>
</tbody>
</table>
<div v-if="users.length === 0" class="no-results">
Пользователи не найдены
<!-- Десктопная таблица (отображается только на больших экранах) -->
<div v-if="!loading && !error" class="desktop-table">
<div class="users-table-container">
<table class="users-table">
<thead>
<tr>
<th>ID</th>
<th>Имя</th>
<th>Email</th>
<th class="hide-sm">Дата регистрации</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user._id">
<td>{{ shortenId(user._id) }}</td>
<td>{{ user.name }}</td>
<td class="email-cell">{{ user.email }}</td>
<td class="hide-sm">{{ formatDate(user.createdAt) }}</td>
<td>
<span class="status-badge" :class="{ 'active': user.isActive, 'blocked': !user.isActive }">
{{ user.isActive ? 'Активен' : 'Заблокирован' }}
</span>
</td>
<td class="actions">
<button @click="viewUser(user._id)" class="btn view-btn">
<span class="btn-text">Просмотр</span>
<i class="bi-eye"></i>
</button>
<button
@click="toggleUserStatus(user._id, user.isActive)"
class="btn"
:class="user.isActive ? 'block-btn' : 'unblock-btn'"
>
<span class="btn-text">{{ user.isActive ? 'Заблокировать' : 'Разблокировать' }}</span>
<i :class="user.isActive ? 'bi-lock-fill' : 'bi-unlock-fill'"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="pagination.pages > 1" class="pagination">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage === 1"
class="pagination-btn"
>
&laquo; Назад
</button>
<!-- Мобильное карточное представление (отображается только на маленьких экранах) -->
<div v-if="!loading && !error" class="mobile-cards">
<div
v-for="user in users"
:key="user._id"
class="user-card"
>
<div class="card-header">
<div class="user-name">{{ user.name }}</div>
<span class="status-badge" :class="{ 'active': user.isActive, 'blocked': !user.isActive }">
{{ user.isActive ? 'Активен' : 'Заблокирован' }}
</span>
</div>
<span class="pagination-info">
Страница {{ currentPage }} из {{ pagination.pages }}
</span>
<div class="card-content">
<div class="info-row">
<div class="info-label">ID:</div>
<div class="info-value id-value">{{ shortenId(user._id) }}</div>
</div>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage === pagination.pages"
class="pagination-btn"
>
Вперёд &raquo;
</button>
<div class="info-row">
<div class="info-label">Email:</div>
<div class="info-value">{{ user.email }}</div>
</div>
<div class="info-row">
<div class="info-label">Дата регистрации:</div>
<div class="info-value">{{ formatDate(user.createdAt) }}</div>
</div>
</div>
<div class="card-actions">
<button @click="viewUser(user._id)" class="card-btn view-btn">
<i class="bi-eye"></i>
<span>Просмотр</span>
</button>
<button
@click="toggleUserStatus(user._id, user.isActive)"
class="card-btn"
:class="user.isActive ? 'block-btn' : 'unblock-btn'"
>
<i :class="user.isActive ? 'bi-lock-fill' : 'bi-unlock-fill'"></i>
<span>{{ user.isActive ? 'Заблокировать' : 'Разблокировать' }}</span>
</button>
</div>
</div>
</div>
<div v-if="!loading && !error && users.length === 0" class="no-results">
<i class="bi-person-slash"></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"
>
<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"
>
<span>Вперёд</span>
<i class="bi-chevron-right"></i>
</button>
</div>
</div>
</template>
@ -264,6 +321,7 @@ export default {
<style scoped>
.admin-users {
padding: 1.5rem;
padding-bottom: 2rem;
max-width: 100%;
}
@ -339,11 +397,30 @@ h2::before {
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
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;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
padding: 1rem;
background-color: #ffebee;
@ -351,11 +428,22 @@ h2::before {
color: #d32f2f;
margin-bottom: 1.5rem;
border-radius: 4px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.error-message i {
color: #f44336;
font-size: 1.2rem;
}
/* Стили для десктопной таблицы */
.users-table-container {
overflow-x: auto;
margin-bottom: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.users-table {
@ -363,8 +451,6 @@ h2::before {
border-collapse: collapse;
min-width: 800px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.users-table th, .users-table td {
@ -467,12 +553,116 @@ h2::before {
background-color: rgba(46, 125, 50, 0.15);
}
.no-results {
padding: 2rem;
text-align: center;
color: #666;
background-color: #f9f9f9;
/* Стили для мобильных карточек */
.mobile-cards {
display: none;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.user-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border: 1px solid #eee;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: #fafafa;
border-bottom: 1px solid #eee;
}
.user-name {
font-weight: 600;
font-size: 1.1rem;
color: #333;
}
.card-content {
padding: 1rem;
}
.info-row {
display: flex;
margin-bottom: 0.5rem;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
flex: 0 0 40%;
color: #666;
font-size: 0.9rem;
}
.info-value {
flex: 1;
font-weight: 500;
}
.id-value {
font-family: monospace;
color: #1976d2;
}
.card-actions {
display: flex;
padding: 0.75rem;
gap: 0.5rem;
border-top: 1px solid #eee;
background-color: #fafafa;
}
.card-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.6rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.card-btn:hover {
transform: translateY(-2px);
}
.no-results {
text-align: center;
padding: 3rem 1rem;
color: #666;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.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 {
@ -493,6 +683,9 @@ h2::before {
transition: all 0.2s ease;
color: #495057;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
}
.pagination-btn:hover:not(:disabled) {
@ -507,122 +700,141 @@ h2::before {
.pagination-info {
color: #6c757d;
font-weight: 500;
}
/* Адаптивные стили */
@media (max-width: 1199px) {
.admin-users {
padding: 1.25rem;
}
}
/* Адаптивное отображение */
@media (max-width: 991px) {
.users-table {
font-size: 0.9rem;
}
.users-table th, .users-table td {
padding: 0.6rem;
}
.btn {
padding: 0.3rem 0.6rem;
}
}
/* Планшеты и маленькие экраны */
@media (max-width: 767px) {
h2 {
font-size: 1.5rem;
}
/* Скрываем менее важные данные на мобильных */
.hide-sm {
/* На планшетах и меньше */
.desktop-table {
display: none;
}
.email-cell {
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.mobile-cards {
display: flex;
}
.admin-users {
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;
}
.filter-options {
justify-content: space-between;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
width: 100%;
}
.users-table {
min-width: auto;
font-size: 0.85rem;
}
.status-badge {
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
}
.btn-text {
display: none;
}
.btn {
padding: 0.4rem;
display: inline-flex;
align-items: center;
.filter-options label {
justify-content: center;
}
.btn i {
font-size: 1.1rem;
margin: 0;
width: 100%;
font-size: 0.9rem;
padding: 0.5rem 0;
}
.pagination {
gap: 0.5rem;
}
.pagination-btn {
padding: 0.4rem 0.7rem;
font-size: 0.85rem;
}
h2 {
font-size: 1.3rem;
margin-bottom: 1rem;
}
/* На мобильных добавляем отступ снизу для нижней навигации */
.admin-users {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
gap: 0.75rem;
}
}
/* Мелкие мобильные устройства */
@media (max-width: 420px) {
.users-table th, .users-table td {
padding: 0.5rem 0.4rem;
}
.email-cell {
max-width: 80px;
@media (max-width: 480px) {
/* Маленькие мобильные устройства */
.admin-users {
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;
}
.filter-options {
grid-template-columns: 1fr 1fr 1fr;
gap: 0.3rem;
}
.filter-options label {
font-size: 0.8rem;
padding: 0.5rem 0.3rem;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.card-actions {
flex-direction: column;
}
.pagination {
flex-direction: row;
justify-content: space-between;
}
.pagination-btn {
padding: 0.5rem;
}
}
/* Устройства с вырезом (notch) */
@supports (padding: env(safe-area-inset-bottom)) {
/* Очень маленькие экраны */
@media (max-width: 360px) {
.admin-users {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
padding: 0.5rem;
padding-bottom: calc(0.5rem + 56px + env(safe-area-inset-bottom, 0px));
}
.info-row {
flex-direction: column;
gap: 0.25rem;
margin-bottom: 0.75rem;
}
.info-label {
flex: none;
}
}
/* Ландшафтная ориентация на мобильных */
@media (max-height: 450px) and (orientation: landscape) {
.users-table {
font-size: 0.8rem;
.admin-users {
padding-bottom: calc(1rem + 50px + env(safe-area-inset-bottom, 0px));
}
.admin-users {
padding-bottom: calc(50px + env(safe-area-inset-bottom, 0px));
.card-actions {
flex-direction: row;
}
}
</style>