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
|
// stores/user.js
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { handleApiError } from './helpers/api'
|
import { apiClient, handleApiError } from './helpers/api'
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
// State
|
// State
|
||||||
const userStats = ref(null)
|
const userStats = ref(null)
|
||||||
const userTraining = ref(null)
|
const userTraining = ref(null)
|
||||||
const userAchievements = ref([])
|
const userAchievements = ref([])
|
||||||
|
const personalBests = ref([])
|
||||||
|
const upcomingEvents = ref([])
|
||||||
|
const currentTrainingPlan = ref(null)
|
||||||
|
const workoutHistory = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
const completedAchievements = computed(() =>
|
const completedAchievements = computed(() =>
|
||||||
userAchievements.value.filter(achievement => achievement.achieved)
|
userAchievements.value.filter(achievement => achievement.verified || achievement.achieved)
|
||||||
)
|
)
|
||||||
|
|
||||||
const pendingAchievements = computed(() =>
|
const pendingAchievements = computed(() =>
|
||||||
userAchievements.value.filter(achievement => !achievement.achieved)
|
userAchievements.value.filter(achievement => !(achievement.verified || achievement.achieved))
|
||||||
)
|
)
|
||||||
|
|
||||||
const achievementProgress = computed(() => {
|
const achievementProgress = computed(() => {
|
||||||
@@ -25,6 +29,22 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
return Math.round((completedAchievements.value.length / userAchievements.value.length) * 100)
|
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
|
// Вспомогательная функция для обработки loading/error
|
||||||
const withStoreLoading = async (fn) => {
|
const withStoreLoading = async (fn) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -40,67 +60,273 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions - Основные данные пользователя
|
||||||
const fetchUserStats = async () => {
|
const fetchUserStats = async () => {
|
||||||
return withStoreLoading(async () => {
|
return withStoreLoading(async () => {
|
||||||
// TODO: Заменить на реальный endpoint когда будет готов
|
try {
|
||||||
// const response = await apiClient.get('/user/stats')
|
const response = await apiClient.get('/user/stats')
|
||||||
|
userStats.value = response.data
|
||||||
// Временные мок данные
|
return { success: true, data: userStats.value }
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
} catch (error) {
|
||||||
|
// Fallback на мок данные если endpoint не готов
|
||||||
userStats.value = {
|
console.warn('Stats endpoint not available, using mock data', error)
|
||||||
totalDistance: 245,
|
userStats.value = {
|
||||||
bestResult: '10км - 48:15',
|
totalDistance: 245.5,
|
||||||
totalWorkouts: 36,
|
totalTime: 12540,
|
||||||
weeklyDistance: 25,
|
avgPace: '5:15',
|
||||||
monthlyDistance: 98,
|
workoutsCount: 36,
|
||||||
avgPace: '5:15',
|
currentStreak: 7,
|
||||||
caloriesBurned: 12450
|
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 () => {
|
const fetchUserAchievements = async () => {
|
||||||
return withStoreLoading(async () => {
|
return withStoreLoading(async () => {
|
||||||
// TODO: Заменить на реальный endpoint когда будет готов
|
try {
|
||||||
// const response = await apiClient.get('/user/achievements')
|
const response = await apiClient.get('/user/achievements')
|
||||||
|
userAchievements.value = response.data
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
return { success: true, data: userAchievements.value }
|
||||||
|
} catch (error) {
|
||||||
userAchievements.value = [
|
console.warn('Achievements endpoint not available, using mock data', error)
|
||||||
{ id: 1, name: 'Первый забег', description: 'Пробежать первую 5км', achieved: true, date: '2024-01-20' },
|
userAchievements.value = [
|
||||||
{ id: 2, name: 'Неделя тренировок', description: 'Тренироваться 7 дней подряд', achieved: true, date: '2024-02-15' },
|
{
|
||||||
{ id: 3, name: '100 км', description: 'Пробежать 100 км', achieved: true, date: '2024-03-01' },
|
id: 1,
|
||||||
{ id: 4, name: 'Полумарафон', description: 'Пробежать 21.1 км', achieved: false },
|
type: 'distance',
|
||||||
{ id: 5, name: 'Скорость', description: 'Пробежать 5км быстрее 25 минут', achieved: false }
|
title: 'Первый забег',
|
||||||
]
|
description: 'Пробежать первую 5км',
|
||||||
|
verified: true,
|
||||||
return { success: true, data: userAchievements.value }
|
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 () => {
|
return withStoreLoading(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchUserStats(),
|
fetchUserStats(),
|
||||||
fetchUserTraining(),
|
fetchUserAchievements(),
|
||||||
fetchUserAchievements()
|
fetchPersonalBests(),
|
||||||
|
fetchUpcomingEvents(),
|
||||||
|
fetchCurrentTrainingPlan(),
|
||||||
|
fetchWorkoutHistory()
|
||||||
])
|
])
|
||||||
return { success: true }
|
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 {
|
return {
|
||||||
// State
|
// State
|
||||||
userStats,
|
userStats,
|
||||||
userTraining,
|
userTraining,
|
||||||
userAchievements,
|
userAchievements,
|
||||||
|
personalBests,
|
||||||
|
upcomingEvents,
|
||||||
|
currentTrainingPlan,
|
||||||
|
workoutHistory,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
|
||||||
@@ -128,11 +374,22 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
completedAchievements,
|
completedAchievements,
|
||||||
pendingAchievements,
|
pendingAchievements,
|
||||||
achievementProgress,
|
achievementProgress,
|
||||||
|
verifiedPersonalBests,
|
||||||
|
confirmedEvents,
|
||||||
|
totalWorkouts,
|
||||||
|
totalCalories,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
fetchUserStats,
|
fetchUserStats,
|
||||||
fetchUserTraining,
|
|
||||||
fetchUserAchievements,
|
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'
|
import AvatarUpload from '../components/AvatarUpload.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
components: {
|
components: {
|
||||||
AvatarUpload
|
AvatarUpload
|
||||||
|
|||||||
Reference in New Issue
Block a user