411 lines
10 KiB
Vue
411 lines
10 KiB
Vue
<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"
|
||
>
|
||
« Назад
|
||
</button>
|
||
|
||
<span class="pagination-info">
|
||
Страница {{ currentPage }} из {{ pagination.pages }}
|
||
</span>
|
||
|
||
<button
|
||
@click="changePage(currentPage + 1)"
|
||
:disabled="currentPage === pagination.pages"
|
||
class="pagination-btn"
|
||
>
|
||
Вперёд »
|
||
</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> |