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">
<div class="form-group">
<label>Заголовок *</label>
<input v-model="newsForm.title" type="text" required placeholder="Введите заголовок новости"
class="form-input">
<input v-model="newsForm.title"
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 class="form-group">
<label>Краткое описание *</label>
<textarea v-model="newsForm.excerpt" required
placeholder="Краткое описание новости (максимум 500 символов)" class="form-textarea" rows="3"
maxlength="500"></textarea>
<div class="char-count">{{ newsForm.excerpt.length }}/500</div>
<textarea v-model="newsForm.excerpt"
required
placeholder="Краткое описание новости (минимум 10 символов, максимум 500)"
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 class="form-group">
<label>Содержание *</label>
<textarea v-model="newsForm.content" required placeholder="Полное содержание новости"
class="form-textarea" rows="6"></textarea>
<textarea v-model="newsForm.content"
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 class="form-group">
<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="events">События</option>
<option value="training">Тренировки</option>
<option value="achievements">Достижения</option>
<option value="community">Сообщество</option>
</select>
<div v-if="formErrors.category" class="error-message">{{ formErrors.category }}</div>
</div>
<div class="form-group">
<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>
</div>
@@ -255,7 +298,9 @@
<button type="button" class="btn btn-outline" @click="closeNewsFormModal">
Отмена
</button>
<button type="submit" class="btn btn-primary" :disabled="newsLoading">
<button type="submit"
class="btn btn-primary"
:disabled="newsLoading || !isFormValid">
{{ newsLoading ? 'Сохранение...' : (editingNews ? '💾 Сохранить изменения' : '📝 Создать новость') }}
</button>
</div>
@@ -296,6 +341,12 @@ export default {
category: '',
image: ''
},
formErrors: {
title: '',
excerpt: '',
content: '',
category: ''
},
filters: [
{ value: 'all', label: 'Все новости' },
{ value: 'events', label: 'События' },
@@ -322,11 +373,74 @@ export default {
filtered = this.news.filter(item => item.category === this.activeFilter)
}
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: {
...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() {
this.loading = true
this.error = ''
@@ -389,7 +503,7 @@ export default {
if (!confirm('Удалить этот комментарий?')) return
try {
await apiClient.$.delete(`/news/comments/${commentId}`)
await apiClient.delete(`/news/comments/${commentId}`)
await this.fetchComments(this.selectedNews.id)
await this.fetchNews()
} catch (error) {
@@ -407,6 +521,7 @@ export default {
category: '',
image: ''
}
this.clearAllErrors()
this.showNewsFormModal = true
},
@@ -419,6 +534,7 @@ export default {
category: newsItem.category,
image: newsItem.image || ''
}
this.clearAllErrors()
this.showNewsFormModal = true
if (this.showNewsModal) {
this.closeNewsModal()
@@ -449,6 +565,12 @@ export default {
},
async submitNewsForm() {
// Предварительная валидация на фронтенде
if (!this.validateForm()) {
alert('Пожалуйста, исправьте ошибки в форме перед отправкой.')
return
}
this.newsLoading = true
try {
if (this.editingNews) {
@@ -462,7 +584,17 @@ export default {
await this.fetchNews()
} catch (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('Не удалось сохранить новость. Проверьте все поля и попробуйте снова.')
}
} finally {
this.newsLoading = false
}
@@ -511,6 +643,7 @@ export default {
closeNewsFormModal() {
this.showNewsFormModal = false
this.editingNews = null
this.clearAllErrors()
},
shareNews(newsItem) {
@@ -560,6 +693,12 @@ export default {
if (this.showNewsModal) this.closeNewsModal()
if (this.showNewsFormModal) this.closeNewsFormModal()
}
},
handleAuthError() {
localStorage.removeItem('auth_token')
this.isAuthenticated = false
this.currentUser = null
}
},
async mounted() {
@@ -578,8 +717,44 @@ export default {
</script>
<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 {
display: flex;
justify-content: space-between;
@@ -783,13 +958,6 @@ export default {
min-height: 100px;
}
.char-count {
text-align: right;
font-size: 0.8rem;
color: #666;
margin-top: 0.25rem;
}
.form-actions {
display: flex;
gap: 1rem;