Reflex/src/views/admin/AdminUsers.vue
Professional 398fe61565 фикс
2025-05-26 00:00:26 +07:00

411 lines
10 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-users">
<h2>Управление пользователями</h2>
<div class="search-bar">
<input
type="text"
v-model="searchQuery"
placeholder="Поиск по имени или email..."
@input="debounceSearch"
/>
<div class="filter-options">
<label>
<input type="radio" v-model="activeFilter" value="all" @change="loadUsers">
Все
</label>
<label>
<input type="radio" v-model="activeFilter" value="active" @change="loadUsers">
Активные
</label>
<label>
<input type="radio" v-model="activeFilter" value="blocked" @change="loadUsers">
Заблокированные
</label>
</div>
</div>
<div v-if="loading" class="loading-indicator">
Загрузка пользователей...
</div>
<div v-if="error" class="error-message">
{{ 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>Дата регистрации</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>{{ user.email }}</td>
<td>{{ 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">
Просмотр
</button>
<button
@click="toggleUserStatus(user._id, user.isActive)"
class="btn"
:class="user.isActive ? 'block-btn' : 'unblock-btn'"
>
{{ user.isActive ? 'Заблокировать' : 'Разблокировать' }}
</button>
</td>
</tr>
</tbody>
</table>
<div v-if="users.length === 0" class="no-results">
Пользователи не найдены
</div>
<div v-if="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>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
export default {
name: 'AdminUsers',
setup() {
const router = useRouter();
const users = ref([]);
const loading = ref(false);
const error = ref(null);
const searchQuery = ref('');
const activeFilter = ref('all');
const currentPage = ref(1);
const pagination = ref({
page: 1,
limit: 20,
total: 0,
pages: 0
});
const loadUsers = async () => {
loading.value = true;
error.value = null;
try {
// Определяем параметры запроса
const params = {
page: currentPage.value,
limit: pagination.value.limit
};
// Добавляем поисковый запрос, если он есть
if (searchQuery.value) {
params.search = searchQuery.value;
}
// Добавляем фильтр по статусу активности
if (activeFilter.value !== 'all') {
params.isActive = activeFilter.value === 'active';
}
const token = localStorage.getItem('userToken'); // Исправлено с 'token' на 'userToken'
const response = await axios.get(`/api/admin/users`, {
params,
headers: {
Authorization: `Bearer ${token}`
}
});
users.value = response.data.users;
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 viewUser = (userId) => {
router.push({ name: 'AdminUserDetail', params: { id: userId } });
};
// Изменение статуса пользователя (блокировка/разблокировка)
const toggleUserStatus = async (userId, currentStatus) => {
try {
loading.value = true;
const token = localStorage.getItem('userToken'); // Исправлено с 'token' на 'userToken'
const response = await axios.put(
`/api/admin/users/${userId}/toggle-active`,
{},
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
// Обновляем статус пользователя в списке
const userIndex = users.value.findIndex(user => user._id === userId);
if (userIndex !== -1) {
users.value[userIndex].isActive = response.data.isActive;
}
// Показываем уведомление (можно добавить компонент уведомлений)
alert(response.data.message);
} catch (err) {
console.error('Ошибка при изменении статуса пользователя:', err);
alert('Ошибка при изменении статуса пользователя');
} finally {
loading.value = false;
}
};
// Изменение страницы пагинации
const changePage = (page) => {
if (page < 1 || page > pagination.value.pages) return;
currentPage.value = page;
loadUsers();
};
// Задержка для поиска (чтобы не отправлять запрос после каждого ввода символа)
let searchTimeout;
const debounceSearch = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentPage.value = 1; // Сбрасываем страницу при новом поиске
loadUsers();
}, 300);
};
onMounted(() => {
loadUsers();
});
return {
users,
loading,
error,
searchQuery,
activeFilter,
currentPage,
pagination,
loadUsers,
shortenId,
formatDate,
viewUser,
toggleUserStatus,
changePage,
debounceSearch
};
}
}
</script>
<style scoped>
.admin-users {
padding-bottom: 2rem;
}
h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #333;
}
.search-bar {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.search-bar input {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
width: 100%;
}
.filter-options {
display: flex;
gap: 1.5rem;
}
.filter-options label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.loading-indicator {
text-align: center;
padding: 2rem;
color: #666;
}
.error-message {
padding: 1rem;
background-color: #ffebee;
color: #d32f2f;
border-radius: 4px;
margin-bottom: 1rem;
}
.users-table-container {
overflow-x: auto;
}
.users-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
.users-table th, .users-table td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.users-table th {
background-color: #f5f5f5;
font-weight: 500;
color: #444;
}
.status-badge {
padding: 0.3rem 0.6rem;
border-radius: 20px;
font-size: 0.875rem;
}
.status-badge.active {
background-color: #e8f5e9;
color: #2e7d32;
}
.status-badge.blocked {
background-color: #ffebee;
color: #d32f2f;
}
.actions {
display: flex;
gap: 0.5rem;
}
.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;
}
.block-btn {
background-color: #ffebee;
color: #d32f2f;
}
.unblock-btn {
background-color: #e8f5e9;
color: #2e7d32;
}
.no-results {
text-align: center;
padding: 2rem;
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;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
color: #666;
}
</style>