modified: serv_nginx/bbvue/src/views/News.vue
new News.vue page
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user