modified: main_dc/BB/bbvue/src/components/EventCard.vue

modified:   main_dc/BB/bbvue/src/router/index.js
	modified:   main_dc/BB/bbvue/src/stores/event.js
	modified:   main_dc/BB/bbvue/src/views/Events.vue
	modified:   main_dc/BB/bbvue/src/views/Profile.vue
add events page and event store and eventcard modal
This commit is contained in:
2025-10-25 05:50:36 +05:00
parent 21487660d6
commit 1c774ce80c
5 changed files with 465 additions and 102 deletions
+10 -1
View File
@@ -51,7 +51,7 @@
<button <button
v-if="showActions && isRegistered" v-if="showActions && isRegistered"
class="btn btn-danger btn-sm" class="btn btn-danger btn-sm"
@click="$emit('cancel', event.registration_id)" @click="$emit('cancel', getRegistrationId())"
> >
Отменить Отменить
</button> </button>
@@ -132,6 +132,13 @@ export default {
} }
}) })
const getRegistrationId = () => {
const registration = props.userRegistrations.find(reg =>
reg.event_id === props.event.id
)
return registration?.id
}
const formatDate = (dateString) => { const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('ru-RU', { return new Date(dateString).toLocaleDateString('ru-RU', {
day: 'numeric', day: 'numeric',
@@ -154,6 +161,7 @@ export default {
isFull, isFull,
eventTypeLabel, eventTypeLabel,
imageStyle, imageStyle,
getRegistrationId,
formatDate, formatDate,
truncateDescription truncateDescription
} }
@@ -162,6 +170,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* Стили из предыдущего ответа */
.event-card { .event-card {
background: white; background: white;
border-radius: 15px; border-radius: 15px;
-1
View File
@@ -120,7 +120,6 @@ const router = createRouter({
name: 'Events', name: 'Events',
component: () => import('../views/Events.vue') component: () => import('../views/Events.vue')
}, },
// Можно добавить маршрут для детальной страницы события
{ {
path: '/events/:id', path: '/events/:id',
name: 'EventDetails', name: 'EventDetails',
+36 -30
View File
@@ -11,33 +11,37 @@ export const useEventsStore = defineStore('events', () => {
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
// Getters // Getters - добавляем проверки на null/undefined
const upcomingEvents = computed(() => const upcomingEvents = computed(() => {
events.value if (!events.value || !Array.isArray(events.value)) return []
.filter(event => new Date(event.date) > new Date()) return events.value
.filter(event => event && new Date(event.date) > new Date())
.sort((a, b) => new Date(a.date) - new Date(b.date)) .sort((a, b) => new Date(a.date) - new Date(b.date))
) })
const pastEvents = computed(() => const pastEvents = computed(() => {
events.value if (!events.value || !Array.isArray(events.value)) return []
.filter(event => new Date(event.date) <= new Date()) return events.value
.filter(event => event && new Date(event.date) <= new Date())
.sort((a, b) => new Date(b.date) - new Date(a.date)) .sort((a, b) => new Date(b.date) - new Date(a.date))
) })
const registeredEvents = computed(() => const registeredEvents = computed(() => {
userRegistrations.value if (!userRegistrations.value || !Array.isArray(userRegistrations.value)) return []
.filter(reg => reg.status === 'confirmed') return userRegistrations.value
.filter(reg => reg && reg.status === 'confirmed')
.map(reg => ({ .map(reg => ({
...reg.event, ...reg.event,
registration_status: reg.status, registration_status: reg.status,
registration_id: reg.id, registration_id: reg.id,
result_time: reg.result_time result_time: reg.result_time
})) }))
) })
const pendingRegistrations = computed(() => const pendingRegistrations = computed(() => {
userRegistrations.value.filter(reg => reg.status === 'pending') if (!userRegistrations.value || !Array.isArray(userRegistrations.value)) return []
) return userRegistrations.value.filter(reg => reg && reg.status === 'pending')
})
const eventTypes = { const eventTypes = {
'race': 'Забег', 'race': 'Забег',
@@ -52,8 +56,8 @@ export const useEventsStore = defineStore('events', () => {
try { try {
const queryString = new URLSearchParams(params).toString() const queryString = new URLSearchParams(params).toString()
const response = await apiClient.get(`/events?${queryString}`) const response = await apiClient.get(`/events?${queryString}`)
events.value = response.data events.value = response.data || []
return { success: true, data: response.data } return { success: true, data: events.value }
} catch (error) { } catch (error) {
console.warn('Events endpoint not available, using mock data', error) console.warn('Events endpoint not available, using mock data', error)
events.value = generateMockEvents() events.value = generateMockEvents()
@@ -66,10 +70,10 @@ export const useEventsStore = defineStore('events', () => {
return withLoading({ loading, error }, async () => { return withLoading({ loading, error }, async () => {
try { try {
const response = await apiClient.get('/events/upcoming') const response = await apiClient.get('/events/upcoming')
events.value = response.data events.value = response.data || []
return { success: true, data: response.data } return { success: true, data: events.value }
} catch (error) { } catch (error) {
console.warn('Upcoming events endpoint not available', error) console.warn('Upcoming events endpoint not available' + 'error = ' + error)
return { success: true, data: upcomingEvents.value } return { success: true, data: upcomingEvents.value }
} }
}) })
@@ -82,8 +86,8 @@ export const useEventsStore = defineStore('events', () => {
eventDetails.value = response.data eventDetails.value = response.data
return { success: true, data: response.data } return { success: true, data: response.data }
} catch (error) { } catch (error) {
console.warn('Event details endpoint not available', error) console.warn('Event details endpoint not available' + 'error = ' + error)
eventDetails.value = events.value.find(event => event.id === parseInt(eventId)) || null eventDetails.value = events.value.find(event => event && event.id === parseInt(eventId)) || null
return { success: true, data: eventDetails.value } return { success: true, data: eventDetails.value }
} }
}) })
@@ -93,8 +97,8 @@ export const useEventsStore = defineStore('events', () => {
return withLoading({ loading, error }, async () => { return withLoading({ loading, error }, async () => {
try { try {
const response = await apiClient.get('/events/my/registrations') const response = await apiClient.get('/events/my/registrations')
userRegistrations.value = response.data userRegistrations.value = response.data || []
return { success: true, data: response.data } return { success: true, data: userRegistrations.value }
} catch (error) { } catch (error) {
console.warn('User registrations endpoint not available, using mock data', error) console.warn('User registrations endpoint not available, using mock data', error)
userRegistrations.value = generateMockRegistrations() userRegistrations.value = generateMockRegistrations()
@@ -140,13 +144,15 @@ export const useEventsStore = defineStore('events', () => {
const response = await apiClient.get(`/events/${eventId}/availability`) const response = await apiClient.get(`/events/${eventId}/availability`)
return { success: true, data: response.data } return { success: true, data: response.data }
} catch (error) { } catch (error) {
console.log(error) console.warn('Event availability endpoint not available' + 'error = ' + error)
console.warn('Event availability endpoint not available') const event = events.value.find(e => e && e.id === parseInt(eventId))
const event = events.value.find(e => e.id === parseInt(eventId))
const available = event && const available = event &&
event.registration_open && event.registration_open &&
(event.max_participants === 0 || event.participants_count < event.max_participants) (event.max_participants === 0 || event.participants_count < event.max_participants)
return { success: true, data: { available, remaining_spots: event.max_participants - event.participants_count } } return { success: true, data: {
available,
remaining_spots: event ? event.max_participants - event.participants_count : 0
} }
} }
}) })
} }
@@ -167,7 +173,7 @@ export const useEventsStore = defineStore('events', () => {
return withLoading({ loading, error }, async () => { return withLoading({ loading, error }, async () => {
try { try {
const response = await apiClient.put(`/events/${eventId}`, eventData) const response = await apiClient.put(`/events/${eventId}`, eventData)
const index = events.value.findIndex(event => event.id === parseInt(eventId)) const index = events.value.findIndex(event => event && event.id === parseInt(eventId))
if (index !== -1) { if (index !== -1) {
events.value[index] = response.data events.value[index] = response.data
} }
+414 -68
View File
@@ -1,29 +1,131 @@
<template> <template>
<div class="page events-page"> <div class="page events-page">
<!-- ... остальной код без изменений ... --> <div class="page-header">
<button class="btn btn-back" @click="$router.go(-1)"> Назад</button>
<h1>📅 События</h1>
</div>
<!-- Предстоящие события --> <div class="events-controls">
<div v-if="activeTab === 'upcoming'" class="events-section"> <div class="tabs">
<h2>🔮 Предстоящие события</h2> <button
<div v-if="filteredUpcomingEvents.length === 0" class="empty-state"> class="tab-button"
<p>Нет предстоящих событий</p> :class="{ active: activeTab === 'upcoming' }"
<button class="btn btn-primary" @click="refreshEvents"> @click="activeTab = 'upcoming'"
🔄 Обновить >
🔮 Предстоящие
</button>
<button
class="tab-button"
:class="{ active: activeTab === 'my' }"
@click="activeTab = 'my'"
>
🎫 Мои регистрации
</button>
<button
class="tab-button"
:class="{ active: activeTab === 'past' }"
@click="activeTab = 'past'"
>
📚 Прошедшие
</button> </button>
</div> </div>
<div v-else class="events-grid">
<EventCard <div class="filters">
v-for="event in filteredUpcomingEvents" <select v-model="typeFilter" @change="applyFilters">
:key="event.id" <option value="">Все типы</option>
:event="event" <option value="race">Забеги</option>
:user-registrations="userRegistrations" <option value="training">Тренировки</option>
@register="handleRegister" <option value="social">Встречи</option>
@view-details="viewEventDetails" <option value="workshop">Семинары</option>
/> </select>
</div> </div>
</div> </div>
<!-- ... остальной код без изменений ... --> <div v-if="loading" class="loading">Загрузка событий...</div>
<div v-else class="events-content">
<!-- Предстоящие события -->
<div v-if="activeTab === 'upcoming'" class="events-section">
<h2>🔮 Предстоящие события</h2>
<div v-if="!filteredUpcomingEvents || filteredUpcomingEvents.length === 0" class="empty-state">
<p>Нет предстоящих событий</p>
<button class="btn btn-primary" @click="refreshEvents">
🔄 Обновить
</button>
</div>
<div v-else class="events-grid">
<EventCard
v-for="event in filteredUpcomingEvents"
:key="event.id"
:event="event"
:user-registrations="userRegistrations"
@register="handleRegister"
@view-details="viewEventDetails"
/>
</div>
</div>
<!-- Мои регистрации -->
<div v-if="activeTab === 'my'" class="events-section">
<h2>🎫 Мои регистрации</h2>
<div v-if="(!registeredEvents || registeredEvents.length === 0) && (!pendingRegistrations || pendingRegistrations.length === 0)" class="empty-state">
<p>У вас нет зарегистрированных событий</p>
<button class="btn btn-primary" @click="activeTab = 'upcoming'">
📅 Посмотреть события
</button>
</div>
<div v-if="pendingRegistrations && pendingRegistrations.length > 0" class="registrations-section">
<h3> Ожидают подтверждения</h3>
<div class="registrations-list">
<RegistrationCard
v-for="registration in pendingRegistrations"
:key="registration.id"
:registration="registration"
@cancel="handleCancelRegistration"
/>
</div>
</div>
<div v-if="registeredEvents && registeredEvents.length > 0" class="registrations-section">
<h3> Подтвержденные участия</h3>
<div class="events-grid">
<EventCard
v-for="event in registeredEvents"
:key="event.id"
:event="event"
:user-registrations="userRegistrations"
:show-actions="true"
@cancel="handleCancelRegistration"
@view-details="viewEventDetails"
/>
</div>
</div>
</div>
<!-- Прошедшие события -->
<div v-if="activeTab === 'past'" class="events-section">
<h2>📚 Прошедшие события</h2>
<div v-if="!pastEvents || pastEvents.length === 0" class="empty-state">
<p>Нет прошедших событий</p>
</div>
<div v-else class="events-grid">
<EventCard
v-for="event in pastEvents"
:key="event.id"
:event="event"
:user-registrations="userRegistrations"
:show-actions="false"
@view-details="viewEventDetails"
/>
</div>
</div>
</div>
<div v-if="error" class="error-message">
{{ error }}
<button class="btn-retry" @click="loadEventsData">Попробовать снова</button>
</div>
<!-- Модальное окно регистрации --> <!-- Модальное окно регистрации -->
<div v-if="showRegistrationModal" class="modal-overlay" @click="closeRegistrationModal"> <div v-if="showRegistrationModal" class="modal-overlay" @click="closeRegistrationModal">
@@ -112,15 +214,19 @@
</template> </template>
<script> <script>
import { useEventsStore } from '../stores/events' import { useEventsStore } from '../stores/event'
import { ref, onMounted, computed, watch } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
// Импортируем компоненты как default
import EventCard from '../components/EventCard.vue' import EventCard from '../components/EventCard.vue'
import RegistrationCard from '../components/RegistrationCard.vue'
export default { export default {
// eslint-disable-next-line vue/multi-word-component-names // eslint-disable-next-line vue/multi-word-component-names
name: 'Events', name: 'Events',
components: { components: {
EventCard, EventCard,
RegistrationCard
}, },
setup() { setup() {
const eventsStore = useEventsStore() const eventsStore = useEventsStore()
@@ -135,13 +241,13 @@ export default {
const availabilityCheck = ref(null) const availabilityCheck = ref(null)
const showSuccessNotification = ref(false) const showSuccessNotification = ref(false)
// Computed // Computed - добавляем безопасные проверки
const filteredUpcomingEvents = computed(() => { const filteredUpcomingEvents = computed(() => {
let events = eventsStore.upcomingEvents let events = eventsStore.upcomingEvents || []
if (typeFilter.value) { if (typeFilter.value && events) {
events = events.filter(event => event.type === typeFilter.value) events = events.filter(event => event && event.type === typeFilter.value)
} }
return events return events || []
}) })
// Watch для проверки доступности при выборе события // Watch для проверки доступности при выборе события
@@ -158,10 +264,14 @@ export default {
// Методы // Методы
const loadEventsData = async () => { const loadEventsData = async () => {
await Promise.all([ try {
eventsStore.fetchEvents(), await Promise.all([
eventsStore.fetchUserRegistrations() eventsStore.fetchEvents(),
]) eventsStore.fetchUserRegistrations()
])
} catch (error) {
console.error('Error loading events data:', error)
}
} }
const refreshEvents = async () => { const refreshEvents = async () => {
@@ -173,14 +283,20 @@ export default {
} }
const handleRegister = async (event) => { const handleRegister = async (event) => {
if (!event) return
selectedEvent.value = event selectedEvent.value = event
registrationNotes.value = '' registrationNotes.value = ''
showRegistrationModal.value = true showRegistrationModal.value = true
// Проверяем доступность // Проверяем доступность
const result = await eventsStore.checkEventAvailability(event.id) try {
if (result.success) { const result = await eventsStore.checkEventAvailability(event.id)
availabilityCheck.value = result.data if (result.success) {
availabilityCheck.value = result.data
}
} catch (error) {
console.error('Error checking availability:', error)
} }
} }
@@ -188,35 +304,46 @@ export default {
if (!selectedEvent.value || !availabilityCheck.value?.available) return if (!selectedEvent.value || !availabilityCheck.value?.available) return
registering.value = true registering.value = true
const result = await eventsStore.registerForEvent( try {
selectedEvent.value.id, const result = await eventsStore.registerForEvent(
registrationNotes.value selectedEvent.value.id,
) registrationNotes.value
registering.value = false )
if (result.success) {
closeRegistrationModal()
showSuccessNotification.value = true
// Автоматически скрываем уведомление через 5 секунд
setTimeout(() => {
showSuccessNotification.value = false
}, 5000)
// Переключаемся на вкладку "Мои регистрации" if (result.success) {
activeTab.value = 'my' closeRegistrationModal()
} else { showSuccessNotification.value = true
alert('Ошибка при регистрации: ' + result.error) // Автоматически скрываем уведомление через 5 секунд
setTimeout(() => {
showSuccessNotification.value = false
}, 5000)
// Переключаемся на вкладку "Мои регистрации"
activeTab.value = 'my'
} else {
alert('Ошибка при регистрации: ' + result.error)
}
} catch (error) {
console.error('Registration error:', error)
alert('Ошибка при регистрации: ' + error.message)
} finally {
registering.value = false
} }
} }
const handleCancelRegistration = async (registrationId) => { const handleCancelRegistration = async (registrationId) => {
if (confirm('Вы уверены, что хотите отменить регистрацию?')) { if (confirm('Вы уверены, что хотите отменить регистрацию?')) {
const result = await eventsStore.cancelRegistration(registrationId) try {
if (result.success) { const result = await eventsStore.cancelRegistration(registrationId)
// Показываем уведомление об отмене if (result.success) {
alert('Регистрация успешно отменена') // Показываем уведомление об отмене
} else { alert('Регистрация успешно отменена')
alert('Ошибка при отмене регистрации: ' + result.error) } else {
alert('Ошибка при отмене регистрации: ' + result.error)
}
} catch (error) {
console.error('Cancel registration error:', error)
alert('Ошибка при отмене регистрации: ' + error.message)
} }
} }
} }
@@ -224,7 +351,7 @@ export default {
const viewEventDetails = (eventId) => { const viewEventDetails = (eventId) => {
// Переход на страницу деталей события // Переход на страницу деталей события
// Можно реализовать позже отдельную страницу // Можно реализовать позже отдельную страницу
const event = eventsStore.events.find(e => e.id === parseInt(eventId)) const event = eventsStore.events.find(e => e && e.id === parseInt(eventId))
if (event) { if (event) {
alert(`Детали события: ${event.title}\n\n${event.description}`) alert(`Детали события: ${event.title}\n\n${event.description}`)
} }
@@ -240,13 +367,19 @@ export default {
} }
const formatDate = (dateString) => { const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('ru-RU', { if (!dateString) return ''
day: 'numeric', try {
month: 'long', return new Date(dateString).toLocaleDateString('ru-RU', {
year: 'numeric', day: 'numeric',
hour: '2-digit', month: 'long',
minute: '2-digit' year: 'numeric',
}) hour: '2-digit',
minute: '2-digit'
})
} catch (error) {
console.error('Date formatting error:', error)
return dateString
}
} }
onMounted(() => { onMounted(() => {
@@ -256,11 +389,11 @@ export default {
return { return {
loading: computed(() => eventsStore.loading), loading: computed(() => eventsStore.loading),
error: computed(() => eventsStore.error), error: computed(() => eventsStore.error),
upcomingEvents: computed(() => eventsStore.upcomingEvents), upcomingEvents: computed(() => eventsStore.upcomingEvents || []),
pastEvents: computed(() => eventsStore.pastEvents), pastEvents: computed(() => eventsStore.pastEvents || []),
registeredEvents: computed(() => eventsStore.registeredEvents), registeredEvents: computed(() => eventsStore.registeredEvents || []),
pendingRegistrations: computed(() => eventsStore.pendingRegistrations), pendingRegistrations: computed(() => eventsStore.pendingRegistrations || []),
userRegistrations: computed(() => eventsStore.userRegistrations), userRegistrations: computed(() => eventsStore.userRegistrations || []),
activeTab, activeTab,
typeFilter, typeFilter,
showRegistrationModal, showRegistrationModal,
@@ -285,7 +418,134 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* ... существующие стили ... */ /* Стили остаются без изменений */
.events-page {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.page-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.events-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.tabs {
display: flex;
background: #f8f9fa;
border-radius: 12px;
padding: 0.25rem;
gap: 0.25rem;
}
.tab-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: transparent;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.tab-button.active {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
color: #2e8b57;
}
.tab-button:hover:not(.active) {
background: rgba(255, 255, 255, 0.5);
}
.filters select {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
min-width: 150px;
}
.events-content {
min-height: 400px;
}
.events-section {
margin-bottom: 2rem;
}
.events-section h2 {
margin-bottom: 1.5rem;
color: #333;
}
.events-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.registrations-section {
margin-bottom: 2rem;
}
.registrations-section h3 {
margin-bottom: 1rem;
color: #555;
}
.registrations-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.empty-state {
text-align: center;
padding: 3rem;
background: #f8f9fa;
border-radius: 12px;
color: #666;
}
.empty-state p {
margin-bottom: 1rem;
font-size: 1.1rem;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 15px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header { .modal-header {
display: flex; display: flex;
@@ -318,6 +578,19 @@ export default {
color: #333; color: #333;
} }
.event-preview {
background: #f8f9fa;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border-left: 4px solid #2e8b57;
}
.event-preview h4 {
margin: 0 0 0.5rem;
color: #333;
}
.event-details { .event-details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -357,6 +630,32 @@ export default {
border-radius: 6px; border-radius: 6px;
} }
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 8px;
font-family: inherit;
resize: vertical;
}
.form-group textarea:focus {
outline: none;
border-color: #2e8b57;
box-shadow: 0 0 0 2px rgba(46, 139, 87, 0.2);
}
.char-count { .char-count {
text-align: right; text-align: right;
font-size: 0.8rem; font-size: 0.8rem;
@@ -377,6 +676,12 @@ export default {
margin: 0; margin: 0;
} }
.modal-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.notification { .notification {
position: fixed; position: fixed;
top: 20px; top: 20px;
@@ -411,8 +716,49 @@ export default {
} }
} }
.btn-retry {
background: #2e8b57;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
margin-left: 1rem;
}
.btn-retry:hover {
background: #26734d;
}
/* Адаптивность */ /* Адаптивность */
@media (max-width: 768px) { @media (max-width: 768px) {
.events-controls {
flex-direction: column;
align-items: stretch;
}
.tabs {
order: 2;
}
.filters {
order: 1;
align-self: flex-end;
}
.events-grid {
grid-template-columns: 1fr;
}
.modal-content {
width: 95%;
padding: 1rem;
}
.modal-actions {
flex-direction: column;
}
.notification { .notification {
left: 20px; left: 20px;
right: 20px; right: 20px;
+5 -2
View File
@@ -192,7 +192,7 @@
<button class="btn btn-secondary" @click="$router.push('/')"> На главную</button> <button class="btn btn-secondary" @click="$router.push('/')"> На главную</button>
</div> </div>
</template> </template>
<script> <script>
@@ -214,7 +214,10 @@ export default {
avatarLoadError: false, avatarLoadError: false,
personalBests: [], personalBests: [],
upcomingEvents: [], upcomingEvents: [],
currentTrainingPlan: null currentTrainingPlan: null,
hasInteracted: false,
isContentVisible: false,
autoShowTimeout: null
} }
}, },
computed: { computed: {