Reflex/src/views/admin/AdminReports.vue
Professional c04306a871 фикс
2025-05-26 00:57:20 +07:00

488 lines
12 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-reports">
<h2>Управление жалобами</h2>
<div class="search-bar">
<div class="filter-options">
<label>
<input type="radio" v-model="statusFilter" value="all" @change="loadReports">
Все
</label>
<label>
<input type="radio" v-model="statusFilter" value="pending" @change="loadReports">
На рассмотрении
</label>
<label>
<input type="radio" v-model="statusFilter" value="resolved" @change="loadReports">
Рассмотренные
</label>
<label>
<input type="radio" v-model="statusFilter" value="dismissed" @change="loadReports">
Отклоненные
</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="reports-table-container">
<table class="reports-table">
<thead>
<tr>
<th>ID</th>
<th>Причина</th>
<th>Жалующийся</th>
<th>На пользователя</th>
<th class="hide-sm">Дата подачи</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="report in reports" :key="report._id">
<td>{{ shortenId(report._id) }}</td>
<td>
<span class="reason-badge" :class="getReasonClass(report.reason)">
{{ getReasonText(report.reason) }}
</span>
</td>
<td>{{ report.reporter?.name || 'Удален' }}</td>
<td>{{ report.reportedUser?.name || 'Удален' }}</td>
<td class="hide-sm">{{ formatDate(report.createdAt) }}</td>
<td>
<span class="status-badge" :class="getStatusClass(report.status)">
{{ getStatusText(report.status) }}
</span>
</td>
<td class="actions">
<button @click="viewReport(report._id)" class="btn view-btn">
<span class="btn-text">Просмотр</span>
<i class="bi-eye"></i>
</button>
</td>
</tr>
</tbody>
</table>
<div v-if="reports.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: 'AdminReports',
setup() {
const router = useRouter();
const reports = ref([]);
const loading = ref(false);
const error = ref(null);
const statusFilter = ref('all');
const currentPage = ref(1);
const pagination = ref({
page: 1,
pages: 1,
total: 0,
limit: 20
});
// Загрузка списка жалоб
const loadReports = async () => {
loading.value = true;
error.value = null;
try {
const params = {
page: currentPage.value,
limit: pagination.value.limit
};
if (statusFilter.value !== 'all') {
params.status = statusFilter.value;
}
const token = localStorage.getItem('userToken');
const response = await axios.get(`/api/admin/reports`, {
params,
headers: {
Authorization: `Bearer ${token}`
}
});
reports.value = response.data.reports;
pagination.value = response.data.pagination;
} catch (err) {
console.error('Ошибка при загрузке жалоб:', err);
error.value = 'Ошибка при загрузке списка жалоб. Пожалуйста, попробуйте позже.';
} finally {
loading.value = false;
}
};
// Функция для сокращения ID
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 getReasonText = (reason) => {
const reasons = {
inappropriate_content: 'Неподходящий контент',
fake_profile: 'Фальшивый профиль',
harassment: 'Домогательства',
spam: 'Спам',
underage: 'Несовершеннолетний',
other: 'Другое'
};
return reasons[reason] || reason;
};
// Получение CSS класса для причины
const getReasonClass = (reason) => {
const classes = {
inappropriate_content: 'reason-inappropriate',
fake_profile: 'reason-fake',
harassment: 'reason-harassment',
spam: 'reason-spam',
underage: 'reason-underage',
other: 'reason-other'
};
return classes[reason] || 'reason-other';
};
// Получение текста статуса
const getStatusText = (status) => {
const statuses = {
pending: 'На рассмотрении',
resolved: 'Рассмотрена',
dismissed: 'Отклонена'
};
return statuses[status] || status;
};
// Получение CSS класса для статуса
const getStatusClass = (status) => {
const classes = {
pending: 'status-pending',
resolved: 'status-resolved',
dismissed: 'status-dismissed'
};
return classes[status] || 'status-pending';
};
// Переход на детальную страницу жалобы
const viewReport = (reportId) => {
router.push({ name: 'AdminReportDetail', params: { id: reportId } });
};
// Изменение страницы пагинации
const changePage = (page) => {
if (page < 1 || page > pagination.value.pages) return;
currentPage.value = page;
loadReports();
};
onMounted(() => {
loadReports();
});
return {
reports,
loading,
error,
statusFilter,
currentPage,
pagination,
loadReports,
shortenId,
formatDate,
getReasonText,
getReasonClass,
getStatusText,
getStatusClass,
viewReport,
changePage
};
}
}
</script>
<style scoped>
.admin-reports {
padding-bottom: 2rem;
max-width: 100%;
}
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 {
margin-bottom: 1.5rem;
}
.filter-options {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.filter-options label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-weight: 500;
color: #555;
}
.filter-options input[type="radio"] {
margin: 0;
}
.loading-indicator {
text-align: center;
padding: 2rem;
color: #666;
font-style: italic;
}
.error-message {
background-color: #f8d7da;
color: #721c24;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
border: 1px solid #f5c6cb;
margin-bottom: 1rem;
}
.reports-table-container {
background: white;
border-radius: 0.75rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.reports-table {
width: 100%;
border-collapse: collapse;
}
.reports-table th,
.reports-table td {
padding: 0.85rem;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.reports-table th {
background: linear-gradient(to right, #f8f9fa, #e9ecef);
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
}
.reports-table tbody tr:hover {
background-color: rgba(255, 62, 104, 0.02);
}
.reason-badge,
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
}
.reason-inappropriate { background-color: #f8d7da; color: #721c24; }
.reason-fake { background-color: #fff3cd; color: #856404; }
.reason-harassment { background-color: #d1ecf1; color: #0c5460; }
.reason-spam { background-color: #d4edda; color: #155724; }
.reason-underage { background-color: #f8d7da; color: #721c24; }
.reason-other { background-color: #e2e3e5; color: #383d41; }
.status-pending { background-color: #fff3cd; color: #856404; }
.status-resolved { background-color: #d4edda; color: #155724; }
.status-dismissed { background-color: #e2e3e5; color: #383d41; }
.actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.4rem 0.75rem;
border: none;
border-radius: 0.375rem;
cursor: pointer;
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s ease;
}
.view-btn {
background-color: #007bff;
color: white;
}
.view-btn:hover {
background-color: #0056b3;
transform: translateY(-1px);
}
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
}
.pagination-btn {
padding: 0.5rem 1rem;
background-color: white;
border: 1px solid #ced4da;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s ease;
color: #495057;
font-weight: 500;
}
.pagination-btn:hover:not(:disabled) {
background-color: #f8f9fa;
border-color: #ced4da;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
color: #6c757d;
}
.no-results {
text-align: center;
padding: 3rem;
color: #6c757d;
font-style: italic;
}
/* Адаптивные стили */
@media (max-width: 767px) {
.hide-sm {
display: none;
}
.reports-table th,
.reports-table td {
padding: 0.6rem 0.5rem;
font-size: 0.85rem;
}
.btn-text {
display: none;
}
.filter-options {
gap: 0.5rem;
}
h2 {
font-size: 1.3rem;
margin-bottom: 1rem;
}
.admin-reports {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
}
}
/* Устройства с вырезом (notch) */
@supports (padding: env(safe-area-inset-bottom)) {
.admin-reports {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
}
}
/* Ландшафтная ориентация на мобильных */
@media (max-height: 450px) and (orientation: landscape) {
.admin-reports {
padding-bottom: calc(50px + env(safe-area-inset-bottom, 0px));
}
}
</style>