modified: serv_nginx/bbvue/src/views/News.vue

new News.vue page
This commit is contained in:
2025-10-16 10:30:17 +05:00
parent bf9336d35a
commit 4a7e0ba364
+188 -20
View File
@@ -216,38 +216,81 @@
<form @submit.prevent="submitNewsForm" class="news-form"> <form @submit.prevent="submitNewsForm" class="news-form">
<div class="form-group"> <div class="form-group">
<label>Заголовок *</label> <label>Заголовок *</label>
<input v-model="newsForm.title" type="text" required placeholder="Введите заголовок новости" <input v-model="newsForm.title"
class="form-input"> type="text"
required
placeholder="Введите заголовок новости (минимум 5 символов)"
class="form-input"
:class="{ 'error': formErrors.title }"
@input="clearFieldError('title')">
<div v-if="formErrors.title" class="error-message">{{ formErrors.title }}</div>
<div class="char-count" :class="{ 'error': newsForm.title.length < 5 }">
{{ newsForm.title.length }}/255
<span v-if="newsForm.title.length < 5" class="min-length-warning">
(минимум 5 символов)
</span>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Краткое описание *</label> <label>Краткое описание *</label>
<textarea v-model="newsForm.excerpt" required <textarea v-model="newsForm.excerpt"
placeholder="Краткое описание новости (максимум 500 символов)" class="form-textarea" rows="3" required
maxlength="500"></textarea> placeholder="Краткое описание новости (минимум 10 символов, максимум 500)"
<div class="char-count">{{ newsForm.excerpt.length }}/500</div> class="form-textarea"
rows="3"
maxlength="500"
:class="{ 'error': formErrors.excerpt }"
@input="clearFieldError('excerpt')"></textarea>
<div v-if="formErrors.excerpt" class="error-message">{{ formErrors.excerpt }}</div>
<div class="char-count" :class="{ 'error': newsForm.excerpt.length < 10 }">
{{ newsForm.excerpt.length }}/500
<span v-if="newsForm.excerpt.length < 10" class="min-length-warning">
(минимум 10 символов)
</span>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Содержание *</label> <label>Содержание *</label>
<textarea v-model="newsForm.content" required placeholder="Полное содержание новости" <textarea v-model="newsForm.content"
class="form-textarea" rows="6"></textarea> required
placeholder="Полное содержание новости (минимум 50 символов)"
class="form-textarea"
rows="6"
:class="{ 'error': formErrors.content }"
@input="clearFieldError('content')"></textarea>
<div v-if="formErrors.content" class="error-message">{{ formErrors.content }}</div>
<div class="char-count" :class="{ 'error': newsForm.content.length < 50 }">
{{ newsForm.content.length }} символов
<span v-if="newsForm.content.length < 50" class="min-length-warning">
(минимум 50 символов)
</span>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Категория *</label> <label>Категория *</label>
<select v-model="newsForm.category" required class="form-select"> <select v-model="newsForm.category"
required
class="form-select"
:class="{ 'error': formErrors.category }"
@change="clearFieldError('category')">
<option value="">Выберите категорию</option> <option value="">Выберите категорию</option>
<option value="events">События</option> <option value="events">События</option>
<option value="training">Тренировки</option> <option value="training">Тренировки</option>
<option value="achievements">Достижения</option> <option value="achievements">Достижения</option>
<option value="community">Сообщество</option> <option value="community">Сообщество</option>
</select> </select>
<div v-if="formErrors.category" class="error-message">{{ formErrors.category }}</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Изображение (URL)</label> <label>Изображение (URL)</label>
<input v-model="newsForm.image" type="url" placeholder="https://example.com/image.jpg" class="form-input"> <input v-model="newsForm.image"
type="url"
placeholder="https://example.com/image.jpg"
class="form-input">
<small>Оставьте пустым для изображения по умолчанию</small> <small>Оставьте пустым для изображения по умолчанию</small>
</div> </div>
@@ -255,7 +298,9 @@
<button type="button" class="btn btn-outline" @click="closeNewsFormModal"> <button type="button" class="btn btn-outline" @click="closeNewsFormModal">
Отмена Отмена
</button> </button>
<button type="submit" class="btn btn-primary" :disabled="newsLoading"> <button type="submit"
class="btn btn-primary"
:disabled="newsLoading || !isFormValid">
{{ newsLoading ? 'Сохранение...' : (editingNews ? '💾 Сохранить изменения' : '📝 Создать новость') }} {{ newsLoading ? 'Сохранение...' : (editingNews ? '💾 Сохранить изменения' : '📝 Создать новость') }}
</button> </button>
</div> </div>
@@ -296,6 +341,12 @@ export default {
category: '', category: '',
image: '' image: ''
}, },
formErrors: {
title: '',
excerpt: '',
content: '',
category: ''
},
filters: [ filters: [
{ value: 'all', label: 'Все новости' }, { value: 'all', label: 'Все новости' },
{ value: 'events', label: 'События' }, { value: 'events', label: 'События' },
@@ -322,11 +373,74 @@ export default {
filtered = this.news.filter(item => item.category === this.activeFilter) filtered = this.news.filter(item => item.category === this.activeFilter)
} }
return filtered.slice(0, this.visibleNews) return filtered.slice(0, this.visibleNews)
},
// Проверка валидности формы
isFormValid() {
return this.newsForm.title.length >= 5 &&
this.newsForm.title.length <= 255 &&
this.newsForm.excerpt.length >= 10 &&
this.newsForm.excerpt.length <= 500 &&
this.newsForm.content.length >= 50 &&
this.newsForm.category !== ''
} }
}, },
methods: { methods: {
...mapActions(useAuthStore, ['fetchProfile']), ...mapActions(useAuthStore, ['fetchProfile']),
// Валидация формы
validateForm() {
this.clearAllErrors()
let isValid = true
// Валидация заголовка
if (this.newsForm.title.length < 5) {
this.formErrors.title = 'Заголовок должен содержать минимум 5 символов'
isValid = false
} else if (this.newsForm.title.length > 255) {
this.formErrors.title = 'Заголовок не должен превышать 255 символов'
isValid = false
}
// Валидация краткого описания
if (this.newsForm.excerpt.length < 10) {
this.formErrors.excerpt = 'Краткое описание должно содержать минимум 10 символов'
isValid = false
} else if (this.newsForm.excerpt.length > 500) {
this.formErrors.excerpt = 'Краткое описание не должно превышать 500 символов'
isValid = false
}
// Валидация содержания
if (this.newsForm.content.length < 50) {
this.formErrors.content = 'Содержание должно содержать минимум 50 символов'
isValid = false
}
// Валидация категории
if (!this.newsForm.category) {
this.formErrors.category = 'Выберите категорию'
isValid = false
}
return isValid
},
// Очистка ошибок
clearAllErrors() {
this.formErrors = {
title: '',
excerpt: '',
content: '',
category: ''
}
},
clearFieldError(field) {
if (this.formErrors[field]) {
this.formErrors[field] = ''
}
},
async fetchNews() { async fetchNews() {
this.loading = true this.loading = true
this.error = '' this.error = ''
@@ -389,7 +503,7 @@ export default {
if (!confirm('Удалить этот комментарий?')) return if (!confirm('Удалить этот комментарий?')) return
try { try {
await apiClient.$.delete(`/news/comments/${commentId}`) await apiClient.delete(`/news/comments/${commentId}`)
await this.fetchComments(this.selectedNews.id) await this.fetchComments(this.selectedNews.id)
await this.fetchNews() await this.fetchNews()
} catch (error) { } catch (error) {
@@ -407,6 +521,7 @@ export default {
category: '', category: '',
image: '' image: ''
} }
this.clearAllErrors()
this.showNewsFormModal = true this.showNewsFormModal = true
}, },
@@ -419,6 +534,7 @@ export default {
category: newsItem.category, category: newsItem.category,
image: newsItem.image || '' image: newsItem.image || ''
} }
this.clearAllErrors()
this.showNewsFormModal = true this.showNewsFormModal = true
if (this.showNewsModal) { if (this.showNewsModal) {
this.closeNewsModal() this.closeNewsModal()
@@ -449,6 +565,12 @@ export default {
}, },
async submitNewsForm() { async submitNewsForm() {
// Предварительная валидация на фронтенде
if (!this.validateForm()) {
alert('Пожалуйста, исправьте ошибки в форме перед отправкой.')
return
}
this.newsLoading = true this.newsLoading = true
try { try {
if (this.editingNews) { if (this.editingNews) {
@@ -462,7 +584,17 @@ export default {
await this.fetchNews() await this.fetchNews()
} catch (error) { } catch (error) {
console.error('Failed to save news:', error) console.error('Failed to save news:', error)
// Обработка ошибок валидации с бэкенда
if (error.response && error.response.status === 400) {
const errorData = error.response.data
if (typeof errorData === 'string' && errorData.includes('Validation failed')) {
alert('Ошибка валидации: проверьте, что все поля соответствуют требованиям (заголовок - минимум 5 символов, описание - минимум 10 символов, содержание - минимум 50 символов).')
} else {
alert('Ошибка при сохранении: ' + (errorData.message || errorData))
}
} else {
alert('Не удалось сохранить новость. Проверьте все поля и попробуйте снова.') alert('Не удалось сохранить новость. Проверьте все поля и попробуйте снова.')
}
} finally { } finally {
this.newsLoading = false this.newsLoading = false
} }
@@ -511,6 +643,7 @@ export default {
closeNewsFormModal() { closeNewsFormModal() {
this.showNewsFormModal = false this.showNewsFormModal = false
this.editingNews = null this.editingNews = null
this.clearAllErrors()
}, },
shareNews(newsItem) { shareNews(newsItem) {
@@ -560,6 +693,12 @@ export default {
if (this.showNewsModal) this.closeNewsModal() if (this.showNewsModal) this.closeNewsModal()
if (this.showNewsFormModal) this.closeNewsFormModal() if (this.showNewsFormModal) this.closeNewsFormModal()
} }
},
handleAuthError() {
localStorage.removeItem('auth_token')
this.isAuthenticated = false
this.currentUser = null
} }
}, },
async mounted() { async mounted() {
@@ -578,8 +717,44 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* Стили из предыдущей версии остаются без изменений, добавляем только новые */ /* Существующие стили остаются без изменений, добавляем только новые для валидации */
.form-input.error,
.form-textarea.error,
.form-select.error {
border-color: #f44336;
background-color: #fff5f5;
}
.error-message {
color: #f44336;
font-size: 0.8rem;
margin-top: 0.25rem;
font-weight: 500;
}
.char-count.error {
color: #f44336;
}
.min-length-warning {
color: #f44336;
font-size: 0.8rem;
font-weight: 500;
}
.char-count {
text-align: right;
font-size: 0.8rem;
color: #666;
margin-top: 0.25rem;
}
.char-count.error .min-length-warning {
color: #f44336;
}
/* Остальные стили остаются без изменений */
.news-controls { .news-controls {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -783,13 +958,6 @@ export default {
min-height: 100px; min-height: 100px;
} }
.char-count {
text-align: right;
font-size: 0.8rem;
color: #666;
margin-top: 0.25rem;
}
.form-actions { .form-actions {
display: flex; display: flex;
gap: 1rem; gap: 1rem;