modified: serv_nginx/bbvue/src/stores/user.js
modified: serv_nginx/bbvue/src/views/Profile.vue create new user store
This commit is contained in:
@@ -1,23 +1,27 @@
|
||||
// stores/user.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { handleApiError } from './helpers/api'
|
||||
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.achieved)
|
||||
userAchievements.value.filter(achievement => achievement.verified || achievement.achieved)
|
||||
)
|
||||
|
||||
const pendingAchievements = computed(() =>
|
||||
userAchievements.value.filter(achievement => !achievement.achieved)
|
||||
userAchievements.value.filter(achievement => !(achievement.verified || achievement.achieved))
|
||||
)
|
||||
|
||||
const achievementProgress = computed(() => {
|
||||
@@ -25,6 +29,22 @@ export const useUserStore = defineStore('user', () => {
|
||||
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
|
||||
@@ -40,67 +60,273 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
// Actions - Основные данные пользователя
|
||||
const fetchUserStats = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
// TODO: Заменить на реальный endpoint когда будет готов
|
||||
// const response = await apiClient.get('/user/stats')
|
||||
|
||||
// Временные мок данные
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
userStats.value = {
|
||||
totalDistance: 245,
|
||||
bestResult: '10км - 48:15',
|
||||
totalWorkouts: 36,
|
||||
weeklyDistance: 25,
|
||||
monthlyDistance: 98,
|
||||
avgPace: '5:15',
|
||||
caloriesBurned: 12450
|
||||
try {
|
||||
const response = await apiClient.get('/user/stats')
|
||||
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 }
|
||||
}
|
||||
|
||||
return { success: true, data: userStats.value }
|
||||
})
|
||||
}
|
||||
|
||||
const fetchUserTraining = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
// TODO: Заменить на реальный endpoint когда будет готов
|
||||
// const response = await apiClient.get('/user/training')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
userTraining.value = {
|
||||
currentWeek: 4,
|
||||
totalWeeks: 12,
|
||||
nextWorkout: '2024-03-20T18:00:00',
|
||||
workouts: [
|
||||
{ id: 1, date: '2024-03-18', type: 'interval', distance: '8km', completed: true },
|
||||
{ id: 2, date: '2024-03-20', type: 'tempo', distance: '10km', completed: false },
|
||||
{ id: 3, date: '2024-03-22', type: 'long', distance: '15km', completed: false }
|
||||
]
|
||||
}
|
||||
|
||||
return { success: true, data: userTraining.value }
|
||||
})
|
||||
}
|
||||
|
||||
const fetchUserAchievements = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
// TODO: Заменить на реальный endpoint когда будет готов
|
||||
// const response = await apiClient.get('/user/achievements')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
userAchievements.value = [
|
||||
{ id: 1, name: 'Первый забег', description: 'Пробежать первую 5км', achieved: true, date: '2024-01-20' },
|
||||
{ id: 2, name: 'Неделя тренировок', description: 'Тренироваться 7 дней подряд', achieved: true, date: '2024-02-15' },
|
||||
{ id: 3, name: '100 км', description: 'Пробежать 100 км', achieved: true, date: '2024-03-01' },
|
||||
{ id: 4, name: 'Полумарафон', description: 'Пробежать 21.1 км', achieved: false },
|
||||
{ id: 5, name: 'Скорость', description: 'Пробежать 5км быстрее 25 минут', achieved: false }
|
||||
]
|
||||
|
||||
return { success: true, data: userAchievements.value }
|
||||
try {
|
||||
const response = await apiClient.get('/user/achievements')
|
||||
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('/personal-bests')
|
||||
personalBests.value = response.data
|
||||
return { success: true, data: personalBests.value }
|
||||
} catch (error) {
|
||||
console.warn('Personal bests endpoint not available, using mock data', error)
|
||||
personalBests.value = [
|
||||
{
|
||||
id: 1,
|
||||
distanceType: '5k',
|
||||
time: '23:45',
|
||||
pace: '4:45',
|
||||
date: '2024-02-15',
|
||||
verified: true,
|
||||
eventName: 'Парковый забег',
|
||||
location: 'Центральный парк'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
distanceType: '10k',
|
||||
time: '48:15',
|
||||
pace: '4:49',
|
||||
date: '2024-03-10',
|
||||
verified: true,
|
||||
eventName: 'Весенний марафон',
|
||||
location: 'Набережная'
|
||||
}
|
||||
]
|
||||
return { success: true, data: personalBests.value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchUpcomingEvents = async () => {
|
||||
return withStoreLoading(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/events/upcoming')
|
||||
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('/training-plans/current')
|
||||
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 }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,18 +335,38 @@ export const useUserStore = defineStore('user', () => {
|
||||
return withStoreLoading(async () => {
|
||||
await Promise.all([
|
||||
fetchUserStats(),
|
||||
fetchUserTraining(),
|
||||
fetchUserAchievements()
|
||||
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,
|
||||
|
||||
@@ -128,11 +374,22 @@ export const useUserStore = defineStore('user', () => {
|
||||
completedAchievements,
|
||||
pendingAchievements,
|
||||
achievementProgress,
|
||||
verifiedPersonalBests,
|
||||
confirmedEvents,
|
||||
totalWorkouts,
|
||||
totalCalories,
|
||||
|
||||
// Actions
|
||||
fetchUserStats,
|
||||
fetchUserTraining,
|
||||
fetchUserAchievements,
|
||||
fetchAllUserData
|
||||
fetchPersonalBests,
|
||||
fetchUpcomingEvents,
|
||||
fetchCurrentTrainingPlan,
|
||||
fetchWorkoutHistory,
|
||||
fetchAllUserData,
|
||||
addPersonalBest,
|
||||
registerForEvent,
|
||||
completeWorkout,
|
||||
resetUserStore
|
||||
}
|
||||
})
|
||||
@@ -199,6 +199,7 @@ import { useUserStore } from '../stores/user'
|
||||
import AvatarUpload from '../components/AvatarUpload.vue'
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line vue/multi-word-component-names
|
||||
name: 'Profile',
|
||||
components: {
|
||||
AvatarUpload
|
||||
|
||||
Reference in New Issue
Block a user