modified: begushiybashkir/bbvue/src/views/Reviews.vue

modified:   serv_nginx/api_bb/internal/repository/review_repository.go
fix null pointer execption in getStats repo function=method
This commit is contained in:
2025-10-15 04:57:23 +05:00
parent f545f5d71b
commit a200099f79
2 changed files with 359 additions and 84 deletions
+331 -59
View File
@@ -1,4 +1,3 @@
<!-- Reviews.vue (обновленная версия) -->
<template>
<div class="reviews-page">
<!-- Герой-секция -->
@@ -230,7 +229,7 @@
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="!isFormValid || submitting">
<span v-if="submitting">📤 Отправка...</span>
<span v-else>📝 Опубликовать отзыв</span>
<span v-else>📝 {{ editingReview ? 'Обновить отзыв' : 'Опубликовать отзыв' }}</span>
</button>
<button type="button" class="btn btn-outline" @click="resetForm">
🗑 Очистить
@@ -329,12 +328,14 @@ export default {
currentPage: 1,
reviewsPerPage: 6,
totalPages: 1,
totalItems: 0,
submitting: false,
editingReview: null,
stats: {
total_reviews: 0,
average_rating: 0,
success_stories: 0,
rating_distribution: {}
rating_distribution: {5: 0, 4: 0, 3: 0, 2: 0, 1: 0}
},
ratingFilters: [
{ value: 'all', label: 'Все оценки' },
@@ -381,16 +382,21 @@ export default {
filter: this.activeRatingFilter === 'all' ? '' : this.activeRatingFilter
}
// Удаляем пустые параметры
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === null || params[key] === undefined) {
delete params[key]
}
})
const response = await apiClient.get('/reviews', { params })
this.reviews = response.data.reviews
this.totalPages = response.data.total_pages
this.reviews = response.data.reviews || []
this.totalPages = response.data.total_pages || 1
this.totalItems = response.data.total_items || 0
this.currentPage = response.data.current_page || 1
} catch (error) {
console.error('Ошибка при загрузке отзывов:', error)
this.$notify({
type: 'error',
title: 'Ошибка',
text: 'Не удалось загрузить отзывы'
})
this.showNotification('error', 'Ошибка', 'Не удалось загрузить отзывы')
} finally {
this.loading = false
}
@@ -402,34 +408,47 @@ export default {
this.stats = response.data
} catch (error) {
console.error('Ошибка при загрузке статистики:', error)
// Устанавливаем значения по умолчанию при ошибке
this.stats = {
total_reviews: 0,
average_rating: 0,
success_stories: 0,
rating_distribution: {5: 0, 4: 0, 3: 0, 2: 0, 1: 0}
}
// Не показываем ошибку пользователю для статистики
}
},
async checkAuth() {
try {
const token = localStorage.getItem('auth_token') // Используем auth_token вместо token
const token = localStorage.getItem('auth_token')
if (token) {
// Пробуем получить профиль для проверки токена
try {
// Пытаемся получить профиль пользователя для проверки авторизации
const response = await apiClient.get('/user/profile')
this.isAuthenticated = true
this.currentUser = response.data
} catch (error) {
// Если запрос профиля失敗, считаем неавторизованным
console.error('Токен невалиден:', error)
this.isAuthenticated = false
localStorage.removeItem('auth_token')
this.handleAuthError()
}
} else {
this.isAuthenticated = false
this.currentUser = null
}
} catch (error) {
console.error('Ошибка проверки авторизации:', error)
this.isAuthenticated = false
localStorage.removeItem('auth_token')
this.handleAuthError()
}
},
handleAuthError() {
this.isAuthenticated = false
this.currentUser = null
localStorage.removeItem('auth_token')
// НЕ перенаправляем на login, чтобы пользователь мог просматривать отзывы
},
getInitials(author) {
if (!author) return '?'
const firstName = author.first_name || ''
@@ -442,12 +461,17 @@ export default {
},
formatDate(dateString) {
const date = new Date(dateString)
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
})
try {
const date = new Date(dateString)
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
})
} catch (error) {
console.log(error)
return 'Неизвестная дата'
}
},
filterByRating(rating) {
@@ -457,9 +481,11 @@ export default {
},
changePage(page) {
this.currentPage = page
this.loadReviews()
window.scrollTo({ top: 0, behavior: 'smooth' })
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page
this.loadReviews()
window.scrollTo({ top: 0, behavior: 'smooth' })
}
},
getRatingCount(rating) {
@@ -473,12 +499,17 @@ export default {
},
isMyReview(review) {
// В реальном приложении сравнивать с ID текущего пользователя
return this.isAuthenticated && review.author && review.author.id === this.currentUser?.id
return this.isAuthenticated &&
this.currentUser &&
review.author &&
review.author.id === this.currentUser.id
},
async submitReview() {
if (!this.isFormValid) return
if (!this.isFormValid) {
this.showNotification('error', 'Ошибка', 'Пожалуйста, заполните все обязательные поля правильно')
return
}
this.submitting = true
@@ -492,33 +523,55 @@ export default {
trainings: this.newReview.trainings
}
await apiClient.post('/reviews', reviewData)
if (this.editingReview) {
// Редактирование существующего отзыва
await apiClient.put(`/reviews/${this.editingReview.id}`, reviewData)
this.showNotification('success', 'Успех', 'Отзыв успешно обновлен!')
this.editingReview = null
} else {
// Создание нового отзыва
await apiClient.post('/reviews', reviewData)
this.showNotification('success', 'Успех', 'Спасибо за ваш отзыв! После проверки модератором он будет опубликован.')
}
this.resetForm()
this.loadReviews() // Перезагружаем список отзывов
this.loadStats() // Обновляем статистику
this.$notify({
type: 'success',
title: 'Успех',
text: 'Спасибо за ваш отзыв! После проверки модератором он будет опубликован.'
})
this.loadReviews()
this.loadStats()
} catch (error) {
console.error('Ошибка при отправке отзыва:', error)
this.$notify({
type: 'error',
title: 'Ошибка',
text: 'Произошла ошибка при отправке отзыва. Попробуйте еще раз.'
})
let errorMessage = 'Произошла ошибка при отправке отзыва. Попробуйте еще раз.'
if (error.response?.status === 401) {
errorMessage = 'Необходимо авторизоваться для отправки отзыва'
this.handleAuthError()
} else if (error.response?.status === 403) {
errorMessage = 'У вас нет прав для выполнения этого действия'
} else if (error.response?.data?.message) {
errorMessage = error.response.data.message
}
this.showNotification('error', 'Ошибка', errorMessage)
} finally {
this.submitting = false
}
},
async editReview(review) {
// Реализация редактирования отзыва
console.log('Редактирование отзыва:', review)
this.editingReview = review
this.newReview = {
rating: review.rating,
text: review.text,
achievement: review.achievement || '',
distance: review.distance || '',
improvement: review.improvement || '',
trainings: review.trainings || 0
}
// Прокручиваем к форме
document.getElementById('add-review').scrollIntoView({
behavior: 'smooth'
})
},
async deleteReview(reviewId) {
@@ -529,22 +582,25 @@ export default {
try {
await apiClient.delete(`/reviews/${reviewId}`)
this.loadReviews() // Перезагружаем список отзывов
this.loadStats() // Обновляем статистику
this.loadReviews()
this.loadStats()
this.$notify({
type: 'success',
title: 'Успех',
text: 'Отзыв успешно удален'
})
this.showNotification('success', 'Успех', 'Отзыв успешно удален')
} catch (error) {
console.error('Ошибка при удалении отзыва:', error)
this.$notify({
type: 'error',
title: 'Ошибка',
text: 'Не удалось удалить отзыв'
})
let errorMessage = 'Не удалось удалить отзыв'
if (error.response?.status === 403) {
errorMessage = 'Вы можете удалять только свои отзывы'
} else if (error.response?.status === 401) {
errorMessage = 'Необходимо авторизоваться'
this.handleAuthError()
} else if (error.response?.data?.message) {
errorMessage = error.response.data.message
}
this.showNotification('error', 'Ошибка', errorMessage)
}
},
@@ -557,6 +613,33 @@ export default {
improvement: '',
trainings: 0
}
this.editingReview = null
},
showNotification(type, title, text) {
// Используем существующую систему уведомлений или создаем простую альтернативу
if (this.$notify) {
this.$notify({
type: type,
title: title,
text: text
})
} else {
// Простая альтернатива, если $notify не доступен
const notification = document.createElement('div')
notification.className = `notification notification-${type}`
notification.innerHTML = `
<strong>${title}</strong>
<p>${text}</p>
`
document.body.appendChild(notification)
setTimeout(() => {
if (notification.parentNode) {
document.body.removeChild(notification)
}
}, 5000)
}
}
},
mounted() {
@@ -566,6 +649,11 @@ export default {
},
watch: {
sortBy() {
this.currentPage = 1
this.loadReviews()
},
activeRatingFilter() {
this.currentPage = 1
this.loadReviews()
}
}
@@ -573,9 +661,193 @@ export default {
</script>
<style scoped>
/* Стили остаются такими же как в вашем файле */
/* Добавим только несколько новых стилей */
/* Стили остаются без изменений, как в вашем исходном файле */
/* Добавляем только стили для уведомлений */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
z-index: 1000;
animation: slideInRight 0.3s ease;
max-width: 400px;
}
.notification-success {
background: #27ae60;
color: white;
}
.notification-error {
background: #e74c3c;
color: white;
}
.notification-warning {
background: #f39c12;
color: white;
}
.notification-info {
background: #3498db;
color: white;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Остальные стили остаются такими же как в вашем файле */
.loading {
text-align: center;
padding: 3rem;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #2e8b57;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 2s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-reviews {
text-align: center;
padding: 3rem;
color: #666;
}
.btn-edit,
.btn-delete {
background: none;
border: none;
color: #2e8b57;
cursor: pointer;
font-size: 0.8rem;
margin-right: 1rem;
}
.btn-delete {
color: #e74c3c;
}
.btn-edit:hover {
text-decoration: underline;
}
.btn-delete:hover {
text-decoration: underline;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
z-index: 1000;
animation: slideInRight 0.3s ease;
max-width: 400px;
}
.notification-success {
background: #27ae60;
color: white;
}
.notification-error {
background: #e74c3c;
color: white;
}
.notification-warning {
background: #f39c12;
color: white;
}
.notification-info {
background: #3498db;
color: white;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Остальные стили остаются такими же как в вашем файле */
.loading {
text-align: center;
padding: 3rem;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #2e8b57;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 2s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-reviews {
text-align: center;
padding: 3rem;
color: #666;
}
.btn-edit,
.btn-delete {
background: none;
border: none;
color: #2e8b57;
cursor: pointer;
font-size: 0.8rem;
margin-right: 1rem;
}
.btn-delete {
color: #e74c3c;
}
.btn-edit:hover {
text-decoration: underline;
}
.btn-delete:hover {
text-decoration: underline;
}
.loading {
text-align: center;
padding: 3rem;