modified: serv_nginx/bbvue/src/stores/auth.js
renamed: serv_nginx/bbvue/src/stores/user.js -> serv_nginx/bbvue/src/stores/user_store.js modified: serv_nginx/bbvue/src/views/Members.vue search-input width is corrected
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
// stores/user.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { apiClient, handleApiError } from './helpers/api'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// State
|
||||
const userStats = ref(null)
|
||||
const userTraining = ref(null)
|
||||
const userAchievements = ref([])
|
||||
const personalBests = ref([])
|
||||
const upcomingEvents = ref([])
|
||||
const currentTrainingPlan = ref(null)
|
||||
const workoutHistory = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// Getters
|
||||
const completedAchievements = computed(() =>
|
||||
userAchievements.value.filter(achievement => achievement.verified || achievement.achieved)
|
||||
)
|
||||
|
||||
const pendingAchievements = computed(() =>
|
||||
userAchievements.value.filter(achievement => !(achievement.verified || achievement.achieved))
|
||||
)
|
||||
|
||||
const achievementProgress = computed(() => {
|
||||
if (!userAchievements.value.length) return 0
|
||||
return Math.round((completedAchievements.value.length / userAchievements.value.length) * 100)
|
||||
})
|
||||
|
||||
const verifiedPersonalBests = computed(() =>
|
||||
personalBests.value.filter(best => best.verified)
|
||||
)
|
||||
|
||||
const confirmedEvents = computed(() =>
|
||||
upcomingEvents.value.filter(event => event.registrationStatus === 'confirmed')
|
||||
)
|
||||
|
||||
const totalWorkouts = computed(() =>
|
||||
userStats.value?.workoutsCount || workoutHistory.value.length
|
||||
)
|
||||
|
||||
const totalCalories = computed(() =>
|
||||
workoutHistory.value.reduce((sum, workout) => sum + (workout.calories || 0), 0)
|
||||
)
|
||||
|
||||
// Вспомогательная функция для обработки loading/error
|
||||
const withStoreLoading = async (fn) => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
const result = handleApiError(err)
|
||||
error.value = result.error
|
||||
return result
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Actions - Основные данные пользователя
|
||||
const fetchUserStats = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/user/stats')
|
||||
console.log("debug /user/stats " + response.data)
|
||||
userStats.value = response.data
|
||||
return { success: true, data: userStats.value }
|
||||
} catch (error) {
|
||||
// Fallback на мок данные если endpoint не готов
|
||||
console.warn('Stats endpoint not available, using mock data', error)
|
||||
userStats.value = {
|
||||
totalDistance: 245.5,
|
||||
totalTime: 12540,
|
||||
avgPace: '5:15',
|
||||
workoutsCount: 36,
|
||||
currentStreak: 7,
|
||||
longestStreak: 21,
|
||||
weeklyDistance: 25.8,
|
||||
monthlyDistance: 98.2,
|
||||
personal_bests: {
|
||||
best_5k: '23:45',
|
||||
best_10k: '48:15',
|
||||
best_half: '1:48:30',
|
||||
best_marathon: null
|
||||
}
|
||||
}
|
||||
return { success: true, data: userStats.value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchUserAchievements = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/user/achievements')
|
||||
console.log("debug /user/achievements " + response.data)
|
||||
userAchievements.value = response.data || []
|
||||
return { success: true, data: userAchievements.value }
|
||||
} catch (error) {
|
||||
console.warn('Achievements endpoint not available, using mock data', error)
|
||||
userAchievements.value = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'distance',
|
||||
title: 'Первый забег',
|
||||
description: 'Пробежать первую 5км',
|
||||
verified: true,
|
||||
date: '2024-01-20',
|
||||
badgeImage: '/badges/first-run.png'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'consistency',
|
||||
title: 'Неделя тренировок',
|
||||
description: 'Тренироваться 7 дней подряд',
|
||||
verified: true,
|
||||
date: '2024-02-15',
|
||||
badgeImage: '/badges/week-streak.png'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'distance',
|
||||
title: '100 км',
|
||||
description: 'Пробежать 100 км',
|
||||
verified: true,
|
||||
date: '2024-03-01',
|
||||
badgeImage: '/badges/100km.png'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'distance',
|
||||
title: 'Полумарафон',
|
||||
description: 'Пробежать 21.1 км',
|
||||
verified: false,
|
||||
badgeImage: '/badges/half-marathon.png'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'speed',
|
||||
title: 'Скорость',
|
||||
description: 'Пробежать 5км быстрее 25 минут',
|
||||
verified: false,
|
||||
badgeImage: '/badges/speedster.png'
|
||||
}
|
||||
]
|
||||
return { success: true, data: userAchievements.value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Новые actions для дополнительных данных
|
||||
const fetchPersonalBests = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/user/personal-bests')
|
||||
console.log("debug /user/personal-bests " + response.data)
|
||||
personalBests.value = response.data
|
||||
return { success: true, data: personalBests.value }
|
||||
} catch (error) {
|
||||
console.warn('Personal bests 1endpoint not available, using mock data', error)
|
||||
personalBests.value = []
|
||||
return { success: true, data: personalBests.value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchUpcomingEvents = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/events/upcoming')
|
||||
console.log("debug /events/upcoming " + response.data)
|
||||
upcomingEvents.value = response.data
|
||||
return { success: true, data: upcomingEvents.value }
|
||||
} catch (error) {
|
||||
console.warn('Events endpoint not available, using mock data', error)
|
||||
upcomingEvents.value = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Летний забег 10км',
|
||||
date: '2024-06-15T09:00:00',
|
||||
location: 'Городской парк',
|
||||
type: 'race',
|
||||
distance: '10 км',
|
||||
registrationStatus: 'confirmed'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Тренировка для начинающих',
|
||||
date: '2024-06-20T18:30:00',
|
||||
location: 'Стадион "Спартак"',
|
||||
type: 'training',
|
||||
registrationStatus: 'confirmed'
|
||||
}
|
||||
]
|
||||
return { success: true, data: upcomingEvents.value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchCurrentTrainingPlan = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/user/training-plans')
|
||||
console.log("debug /user/training-plans/active " + response.data)
|
||||
currentTrainingPlan.value = response.data
|
||||
return { success: true, data: currentTrainingPlan.value }
|
||||
} catch (error) {
|
||||
console.warn('Training plan endpoint not available, using mock data', error)
|
||||
currentTrainingPlan.value = {
|
||||
id: 1,
|
||||
title: 'Подготовка к полумарафону',
|
||||
description: '12-недельный план подготовки к первому полумарафону',
|
||||
weeks: 12,
|
||||
currentWeek: 4,
|
||||
workoutsPerWeek: 3,
|
||||
targetDistance: '21.1 км',
|
||||
targetDate: '2024-08-15',
|
||||
completed: false,
|
||||
workouts: [
|
||||
{ id: 1, week: 4, day: 1, type: 'easy', distance: 5, completed: true },
|
||||
{ id: 2, week: 4, day: 3, type: 'tempo', distance: 8, completed: false },
|
||||
{ id: 3, week: 4, day: 5, type: 'long', distance: 12, completed: false }
|
||||
]
|
||||
}
|
||||
return { success: true, data: currentTrainingPlan.value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchWorkoutHistory = async (limit = 10) => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(`/workouts?limit=${limit}`)
|
||||
workoutHistory.value = response.data
|
||||
return { success: true, data: workoutHistory.value }
|
||||
} catch (error) {
|
||||
console.warn('Workouts endpoint not available, using mock data', error)
|
||||
workoutHistory.value = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'easy',
|
||||
distance_km: 5.2,
|
||||
duration_min: 28,
|
||||
pace: '5:23',
|
||||
calories: 320,
|
||||
date: '2024-03-18'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'interval',
|
||||
distance_km: 8.1,
|
||||
duration_min: 42,
|
||||
pace: '5:11',
|
||||
calories: 510,
|
||||
date: '2024-03-16'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'long',
|
||||
distance_km: 15.5,
|
||||
duration_min: 85,
|
||||
pace: '5:29',
|
||||
calories: 980,
|
||||
date: '2024-03-14'
|
||||
}
|
||||
]
|
||||
return { success: true, data: workoutHistory.value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Специализированные методы для обновления данных
|
||||
const addPersonalBest = async (bestData) => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.post('/personal-bests', bestData)
|
||||
personalBests.value.push(response.data)
|
||||
return { success: true, data: response.data }
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const registerForEvent = async (eventId) => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.post('/events/register', { event_id: eventId })
|
||||
// Обновляем список событий
|
||||
await fetchUpcomingEvents()
|
||||
return { success: true, data: response.data }
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const completeWorkout = async (workoutId) => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.patch(`/workouts/${workoutId}/complete`)
|
||||
// Обновляем историю тренировок и статистику
|
||||
await Promise.all([
|
||||
fetchWorkoutHistory(),
|
||||
fetchUserStats()
|
||||
])
|
||||
return { success: true, data: response.data }
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Пакетная загрузка всех данных пользователя
|
||||
const fetchAllUserData = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
await Promise.all([
|
||||
fetchUserStats(),
|
||||
fetchUserAchievements(),
|
||||
fetchPersonalBests(),
|
||||
fetchUpcomingEvents(),
|
||||
fetchCurrentTrainingPlan(),
|
||||
fetchWorkoutHistory()
|
||||
])
|
||||
return { success: true }
|
||||
})
|
||||
}
|
||||
|
||||
// Сброс store
|
||||
const resetUserStore = () => {
|
||||
userStats.value = null
|
||||
userTraining.value = null
|
||||
userAchievements.value = []
|
||||
personalBests.value = []
|
||||
upcomingEvents.value = []
|
||||
currentTrainingPlan.value = null
|
||||
workoutHistory.value = []
|
||||
loading.value = false
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
userStats,
|
||||
userTraining,
|
||||
userAchievements,
|
||||
personalBests,
|
||||
upcomingEvents,
|
||||
currentTrainingPlan,
|
||||
workoutHistory,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// Getters
|
||||
completedAchievements,
|
||||
pendingAchievements,
|
||||
achievementProgress,
|
||||
verifiedPersonalBests,
|
||||
confirmedEvents,
|
||||
totalWorkouts,
|
||||
totalCalories,
|
||||
|
||||
// Actions
|
||||
fetchUserStats,
|
||||
fetchUserAchievements,
|
||||
fetchPersonalBests,
|
||||
fetchUpcomingEvents,
|
||||
fetchCurrentTrainingPlan,
|
||||
fetchWorkoutHistory,
|
||||
fetchAllUserData,
|
||||
addPersonalBest,
|
||||
registerForEvent,
|
||||
completeWorkout,
|
||||
resetUserStore
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user