modified: serv_nginx/bbvue/src/views/Profile.vue

modify Profile page
This commit is contained in:
2025-10-17 09:40:03 +05:00
parent 17dac03dac
commit 6de3abbbaa
+400 -37
View File
@@ -6,11 +6,11 @@
<div v-else-if="user" class="profile-content"> <div v-else-if="user" class="profile-content">
<div class="profile-header"> <div class="profile-header">
<!-- Обновленная секция аватара --> <!-- Секция аватара -->
<div class="avatar-section"> <div class="avatar-section">
<div class="avatar-preview"> <div class="avatar-preview">
<img v-if="user.avatar" :src="avatarUrl" :alt="`Аватар ${user.firstName} ${user.lastName}`" <img v-if="user.avatar && !avatarLoadError" :src="avatarUrl"
class="avatar-image" @error="handleAvatarError"> :alt="`Аватар ${user.firstName} ${user.lastName}`" class="avatar-image" @error="handleAvatarError">
<div v-else class="avatar-placeholder"> <div v-else class="avatar-placeholder">
👤 👤
</div> </div>
@@ -24,7 +24,7 @@
<p v-if="user.phone" class="user-phone">📱 {{ user.phone }}</p> <p v-if="user.phone" class="user-phone">📱 {{ user.phone }}</p>
</div> </div>
<!-- Остальной код остается без изменений --> <!-- Основная информация -->
<div class="profile-info"> <div class="profile-info">
<h3>📋 Информация о пользователе</h3> <h3>📋 Информация о пользователе</h3>
<div class="info-grid"> <div class="info-grid">
@@ -47,6 +47,7 @@
</div> </div>
</div> </div>
<!-- Статистика -->
<div class="profile-stats"> <div class="profile-stats">
<div class="stats-header"> <div class="stats-header">
<h3>📊 Моя статистика</h3> <h3>📊 Моя статистика</h3>
@@ -65,20 +66,60 @@
<p>{{ userStats?.totalDistance || 0 }} км</p> <p>{{ userStats?.totalDistance || 0 }} км</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h4> Лучший результат</h4> <h4> Общее время</h4>
<p>{{ userStats?.bestResult || 'Нет данных' }}</p> <p>{{ formatTime(userStats?.totalTime) }}</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h4>📅 Тренировок</h4> <h4>📅 Тренировок</h4>
<p>{{ userStats?.totalWorkouts || 0 }}</p> <p>{{ userStats?.workoutsCount || 0 }}</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h4>🔥 Сожжено калорий</h4> <h4>🔥 Текущая серия</h4>
<p>{{ userStats?.caloriesBurned || 0 }}</p> <p>{{ userStats?.currentStreak || 0 }} дней</p>
</div>
<div class="stat-card">
<h4> Лучшая серия</h4>
<p>{{ userStats?.longestStreak || 0 }} дней</p>
</div>
<div class="stat-card">
<h4>📈 Пробег за неделю</h4>
<p>{{ userStats?.weeklyDistance || 0 }} км</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Личные рекорды -->
<div class="personal-bests-section" v-if="personalBests.length > 0">
<h3> Личные рекорды</h3>
<div class="bests-grid">
<div v-for="best in personalBests" :key="best.id" class="best-card">
<div class="best-type">{{ getDistanceLabel(best.distanceType) }}</div>
<div class="best-time">{{ best.time }}</div>
<div class="best-date">{{ formatDate(best.date) }}</div>
<div v-if="best.eventName" class="best-event">{{ best.eventName }}</div>
<span v-if="best.verified" class="verified-badge"> Проверен</span>
</div>
</div>
</div>
<!-- Ближайшие события -->
<div class="events-section" v-if="upcomingEvents.length > 0">
<h3>📅 Ближайшие события</h3>
<div class="events-list">
<div v-for="event in upcomingEvents" :key="event.id" class="event-card">
<div class="event-date">{{ formatEventDate(event.date) }}</div>
<div class="event-title">{{ event.title }}</div>
<div class="event-location">📍 {{ event.location }}</div>
<div class="event-distance" v-if="event.distance">{{ event.distance }}</div>
<span class="event-type" :class="event.type">{{ getEventTypeLabel(event.type) }}</span>
</div>
</div>
<button class="btn btn-outline" @click="$router.push('/events')">
📋 Все события
</button>
</div>
<!-- Достижения -->
<div class="achievements-preview"> <div class="achievements-preview">
<h3>🏆 Достижения</h3> <h3>🏆 Достижения</h3>
<div class="achievements-progress"> <div class="achievements-progress">
@@ -90,15 +131,53 @@
<div class="achievements-count"> <div class="achievements-count">
<span>Получено: {{ completedAchievements.length }} из {{ userAchievements.length }}</span> <span>Получено: {{ completedAchievements.length }} из {{ userAchievements.length }}</span>
</div> </div>
<div v-if="recentAchievements.length > 0" class="recent-achievements">
<h4>Последние достижения:</h4>
<div class="achievements-list">
<div v-for="achievement in recentAchievements" :key="achievement.id" class="achievement-badge">
<span class="achievement-icon">🏅</span>
<span class="achievement-title">{{ achievement.title }}</span>
</div>
</div>
</div>
<button class="btn btn-outline" @click="$router.push('/achievements')"> <button class="btn btn-outline" @click="$router.push('/achievements')">
📜 Все достижения 📜 Все достижения
</button> </button>
</div> </div>
<!-- План тренировок -->
<div class="training-plan-section" v-if="currentTrainingPlan">
<h3>📅 Мой план тренировок</h3>
<div class="plan-card">
<div class="plan-title">{{ currentTrainingPlan.title }}</div>
<div class="plan-progress">
<div class="progress-info">
<span>Неделя {{ currentTrainingPlan.currentWeek }} из {{ currentTrainingPlan.weeks }}</span>
<span>{{ Math.round((currentTrainingPlan.currentWeek / currentTrainingPlan.weeks) * 100) }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill"
:style="{ width: Math.round((currentTrainingPlan.currentWeek / currentTrainingPlan.weeks) * 100) + '%' }">
</div>
</div>
</div>
<div class="plan-target" v-if="currentTrainingPlan.targetDistance">
Цель: {{ currentTrainingPlan.targetDistance }}
</div>
<button class="btn" @click="$router.push('/training')">
📊 Продолжить тренировки
</button>
</div>
</div>
<!-- Действия -->
<div class="profile-actions"> <div class="profile-actions">
<button class="btn" @click="editProfile"> Редактировать профиль</button> <button class="btn" @click="editProfile"> Редактировать профиль</button>
<button class="btn" @click="viewDetailedStats">📊 Подробная статистика</button> <button class="btn" @click="viewDetailedStats">📊 Подробная статистика</button>
<button class="btn" @click="$router.push('/training')">📅 Мой план тренировок</button> <button class="btn" @click="$router.push('/personal-bests')"> Мои рекорды</button>
<button class="btn" @click="$router.push('/events')">📅 События</button>
<button class="btn btn-logout" @click="handleLogout" :disabled="authLoading"> <button class="btn btn-logout" @click="handleLogout" :disabled="authLoading">
{{ authLoading ? 'Выход...' : '🚪 Выйти' }} {{ authLoading ? 'Выход...' : '🚪 Выйти' }}
</button> </button>
@@ -117,10 +196,13 @@
<script> <script>
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import { useUserStore } from '../stores/user' import { useUserStore } from '../stores/user'
import AvatarUpload from '../components/AvatarUpload.vue'
export default { export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Profile', name: 'Profile',
components: {
AvatarUpload
},
setup() { setup() {
const authStore = useAuthStore() const authStore = useAuthStore()
const userStore = useUserStore() const userStore = useUserStore()
@@ -130,7 +212,10 @@ export default {
return { return {
authLoading: false, authLoading: false,
statsLoading: false, statsLoading: false,
avatarLoadError: false avatarLoadError: false,
personalBests: [],
upcomingEvents: [],
currentTrainingPlan: null
} }
}, },
computed: { computed: {
@@ -146,26 +231,26 @@ export default {
completedAchievements() { completedAchievements() {
return this.userStore.completedAchievements return this.userStore.completedAchievements
}, },
recentAchievements() {
return this.userAchievements
.filter(a => a.verified)
.sort((a, b) => new Date(b.date) - new Date(a.date))
.slice(0, 3)
},
achievementProgress() { achievementProgress() {
return this.userStore.achievementProgress return this.userStore.achievementProgress
}, },
statsError() { statsError() {
return this.userStore.error return this.userStore.error
}, },
// Вычисляем полный URL аватара
avatarUrl() { avatarUrl() {
if (!this.user?.avatar) return null; if (!this.user?.avatar) return null;
let filename = this.user.avatar.trim('/').split('/').pop(); let filename = this.user.avatar.trim('/').split('/').pop();
// Иначе формируем полный URL
const baseUrl = 'https://begushiybashkir.ru/api/v1/user/avatars/'; const baseUrl = 'https://begushiybashkir.ru/api/v1/user/avatars/';
return baseUrl + filename; return baseUrl + filename;
}, },
joinDate() { joinDate() {
if (!this.user?.createdAt) return 'января 2024'; if (!this.user?.createdAt) return 'января 2024';
const date = new Date(this.user.createdAt); const date = new Date(this.user.createdAt);
const month = date.toLocaleString('ru-RU', { month: 'long' }); const month = date.toLocaleString('ru-RU', { month: 'long' });
const year = date.getFullYear(); const year = date.getFullYear();
@@ -196,14 +281,10 @@ export default {
}, },
methods: { methods: {
async onAvatarUpdated() { async onAvatarUpdated() {
// Сбрасываем флаг ошибки при обновлении аватара
this.avatarLoadError = false; this.avatarLoadError = false;
// Принудительно обновляем профиль
await this.authStore.fetchProfile(); await this.authStore.fetchProfile();
console.log('Avatar updated, user data:', this.authStore.user);
}, },
// Обработчик ошибки загрузки изображения
handleAvatarError() { handleAvatarError() {
console.error('Ошибка загрузки аватара:', this.avatarUrl); console.error('Ошибка загрузки аватара:', this.avatarUrl);
this.avatarLoadError = true; this.avatarLoadError = true;
@@ -214,7 +295,7 @@ export default {
this.avatarLoadError = false; this.avatarLoadError = false;
try { try {
await this.authStore.fetchProfile(); await this.authStore.fetchProfile();
await this.loadStats(); await this.loadExtendedData();
} catch (error) { } catch (error) {
console.error('Ошибка загрузки данных:', error); console.error('Ошибка загрузки данных:', error);
} finally { } finally {
@@ -222,29 +303,101 @@ export default {
} }
}, },
async loadStats() { async loadExtendedData() {
this.statsLoading = true; this.statsLoading = true;
try { try {
const [statsResult, achievementsResult] = await Promise.all([ const [statsResult, achievementsResult, bestsResult, eventsResult, planResult] = await Promise.all([
this.userStore.fetchUserStats(), this.userStore.fetchUserStats(),
this.userStore.fetchUserAchievements() this.userStore.fetchUserAchievements(),
this.fetchPersonalBests(),
this.fetchUpcomingEvents(),
this.fetchCurrentTrainingPlan()
]); ]);
if (!statsResult.success) { if (!statsResult.success) console.error('Ошибка статистики:', statsResult.error);
console.error('Ошибка загрузки статистики:', statsResult.error); if (!achievementsResult.success) console.error('Ошибка достижений:', achievementsResult.error);
} if (!bestsResult.success) console.error('Ошибка рекордов:', bestsResult.error);
if (!achievementsResult.success) { if (!eventsResult.success) console.error('Ошибка событий:', eventsResult.error);
console.error('Ошибка загрузки достижений:', achievementsResult.error); if (!planResult.success) console.error('Ошибка плана:', planResult.error);
}
} catch (error) { } catch (error) {
console.error('Ошибка загрузки статистики:', error); console.error('Ошибка загрузки расширенных данных:', error);
} finally { } finally {
this.statsLoading = false; this.statsLoading = false;
} }
}, },
async fetchPersonalBests() {
try {
const response = await this.userStore.apiClient.get('/personal-bests');
this.personalBests = response.data;
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
},
async fetchUpcomingEvents() {
try {
const response = await this.userStore.apiClient.get('/events/upcoming');
this.upcomingEvents = response.data;
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
},
async fetchCurrentTrainingPlan() {
try {
const response = await this.userStore.apiClient.get('/training-plans/current');
this.currentTrainingPlan = response.data;
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
},
formatTime(minutes) {
if (!minutes) return '0 мин';
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}ч ${mins}мин` : `${mins} мин`;
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('ru-RU');
},
formatEventDate(dateString) {
return new Date(dateString).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long'
});
},
getDistanceLabel(distanceType) {
const labels = {
'5k': '5 км',
'10k': '10 км',
'half_marathon': 'Полумарафон',
'marathon': 'Марафон',
'other': 'Другая дистанция'
};
return labels[distanceType] || distanceType;
},
getEventTypeLabel(eventType) {
const labels = {
'race': 'Забег',
'training': 'Тренировка',
'social': 'Встреча',
'workshop': 'Семинар'
};
return labels[eventType] || eventType;
},
async refreshStats() { async refreshStats() {
await this.loadStats(); await this.loadExtendedData();
}, },
async handleLogout() { async handleLogout() {
@@ -257,21 +410,231 @@ export default {
}, },
viewDetailedStats() { viewDetailedStats() {
// TODO: Переход на страницу детальной статистики this.$router.push('/stats/detailed');
alert('Функция в разработке');
} }
}, },
async mounted() { async mounted() {
if (!this.user) { if (!this.user) {
await this.loadUserData(); await this.loadUserData();
} else { } else {
await this.loadStats(); await this.loadExtendedData();
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
/* Добавляем новые стили для дополнительных секций */
.personal-bests-section,
.events-section,
.training-plan-section {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.bests-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.best-card {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
padding: 1rem;
border-radius: 10px;
text-align: center;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.best-card:hover {
transform: translateY(-3px);
border-color: #2e8b57;
}
.best-type {
font-weight: bold;
color: #2e8b57;
margin-bottom: 0.5rem;
}
.best-time {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.best-date {
color: #666;
font-size: 0.9rem;
}
.best-event {
color: #555;
font-style: italic;
margin-top: 0.5rem;
}
.verified-badge {
background: #2e8b57;
color: white;
padding: 0.2rem 0.5rem;
border-radius: 12px;
font-size: 0.7rem;
margin-top: 0.5rem;
display: inline-block;
}
.events-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin: 1rem 0;
}
.event-card {
background: #f8f9fa;
padding: 1rem;
border-radius: 10px;
border-left: 4px solid #2e8b57;
position: relative;
}
.event-date {
font-weight: bold;
color: #2e8b57;
}
.event-title {
font-weight: 600;
margin: 0.25rem 0;
}
.event-location {
color: #666;
font-size: 0.9rem;
}
.event-distance {
background: #e9ecef;
padding: 0.2rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
display: inline-block;
margin-top: 0.5rem;
}
.event-type {
position: absolute;
top: 1rem;
right: 1rem;
background: #6c757d;
color: white;
padding: 0.2rem 0.5rem;
border-radius: 12px;
font-size: 0.7rem;
}
.event-type.race {
background: #dc3545;
}
.event-type.training {
background: #2e8b57;
}
.event-type.social {
background: #fd7e14;
}
.event-type.workshop {
background: #6f42c1;
}
.plan-card {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
padding: 1.5rem;
border-radius: 12px;
margin: 1rem 0;
border: 2px solid #e9ecef;
}
.plan-title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 1rem;
color: #333;
}
.plan-progress {
margin-bottom: 1rem;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: #666;
}
.plan-target {
background: #2e8b57;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
display: inline-block;
margin-bottom: 1rem;
font-weight: 500;
}
.recent-achievements {
margin: 1rem 0;
}
.achievements-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 0.5rem 0;
}
.achievement-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #ffd700;
}
.achievement-icon {
font-size: 1.2rem;
}
.achievement-title {
font-weight: 500;
}
/* Адаптивность */
@media (max-width: 768px) {
.bests-grid {
grid-template-columns: 1fr;
}
.event-type {
position: static;
margin-top: 0.5rem;
display: inline-block;
}
}
.page { .page {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;