modified: serv_nginx/bbvue/src/views/Profile.vue
modify Profile page
This commit is contained in:
@@ -6,11 +6,11 @@
|
||||
|
||||
<div v-else-if="user" class="profile-content">
|
||||
<div class="profile-header">
|
||||
<!-- Обновленная секция аватара -->
|
||||
<!-- Секция аватара -->
|
||||
<div class="avatar-section">
|
||||
<div class="avatar-preview">
|
||||
<img v-if="user.avatar" :src="avatarUrl" :alt="`Аватар ${user.firstName} ${user.lastName}`"
|
||||
class="avatar-image" @error="handleAvatarError">
|
||||
<img v-if="user.avatar && !avatarLoadError" :src="avatarUrl"
|
||||
:alt="`Аватар ${user.firstName} ${user.lastName}`" class="avatar-image" @error="handleAvatarError">
|
||||
<div v-else class="avatar-placeholder">
|
||||
👤
|
||||
</div>
|
||||
@@ -24,7 +24,7 @@
|
||||
<p v-if="user.phone" class="user-phone">📱 {{ user.phone }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Остальной код остается без изменений -->
|
||||
<!-- Основная информация -->
|
||||
<div class="profile-info">
|
||||
<h3>📋 Информация о пользователе</h3>
|
||||
<div class="info-grid">
|
||||
@@ -47,6 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статистика -->
|
||||
<div class="profile-stats">
|
||||
<div class="stats-header">
|
||||
<h3>📊 Моя статистика</h3>
|
||||
@@ -65,20 +66,60 @@
|
||||
<p>{{ userStats?.totalDistance || 0 }} км</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>⭐ Лучший результат</h4>
|
||||
<p>{{ userStats?.bestResult || 'Нет данных' }}</p>
|
||||
<h4>⏱️ Общее время</h4>
|
||||
<p>{{ formatTime(userStats?.totalTime) }}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>📅 Тренировок</h4>
|
||||
<p>{{ userStats?.totalWorkouts || 0 }}</p>
|
||||
<p>{{ userStats?.workoutsCount || 0 }}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>🔥 Сожжено калорий</h4>
|
||||
<p>{{ userStats?.caloriesBurned || 0 }}</p>
|
||||
<h4>🔥 Текущая серия</h4>
|
||||
<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 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">
|
||||
<h3>🏆 Достижения</h3>
|
||||
<div class="achievements-progress">
|
||||
@@ -90,15 +131,53 @@
|
||||
<div class="achievements-count">
|
||||
<span>Получено: {{ completedAchievements.length }} из {{ userAchievements.length }}</span>
|
||||
</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>
|
||||
</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">
|
||||
<button class="btn" @click="editProfile">✏️ Редактировать профиль</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">
|
||||
{{ authLoading ? 'Выход...' : '🚪 Выйти' }}
|
||||
</button>
|
||||
@@ -117,10 +196,13 @@
|
||||
<script>
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
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
|
||||
},
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
@@ -130,7 +212,10 @@ export default {
|
||||
return {
|
||||
authLoading: false,
|
||||
statsLoading: false,
|
||||
avatarLoadError: false
|
||||
avatarLoadError: false,
|
||||
personalBests: [],
|
||||
upcomingEvents: [],
|
||||
currentTrainingPlan: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -146,26 +231,26 @@ export default {
|
||||
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() {
|
||||
return this.userStore.achievementProgress
|
||||
},
|
||||
statsError() {
|
||||
return this.userStore.error
|
||||
},
|
||||
// Вычисляем полный URL аватара
|
||||
avatarUrl() {
|
||||
if (!this.user?.avatar) return null;
|
||||
|
||||
let filename = this.user.avatar.trim('/').split('/').pop();
|
||||
|
||||
// Иначе формируем полный URL
|
||||
const baseUrl = 'https://begushiybashkir.ru/api/v1/user/avatars/';
|
||||
|
||||
return baseUrl + filename;
|
||||
},
|
||||
joinDate() {
|
||||
if (!this.user?.createdAt) return 'января 2024';
|
||||
|
||||
const date = new Date(this.user.createdAt);
|
||||
const month = date.toLocaleString('ru-RU', { month: 'long' });
|
||||
const year = date.getFullYear();
|
||||
@@ -196,14 +281,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async onAvatarUpdated() {
|
||||
// Сбрасываем флаг ошибки при обновлении аватара
|
||||
this.avatarLoadError = false;
|
||||
// Принудительно обновляем профиль
|
||||
await this.authStore.fetchProfile();
|
||||
console.log('Avatar updated, user data:', this.authStore.user);
|
||||
},
|
||||
|
||||
// Обработчик ошибки загрузки изображения
|
||||
handleAvatarError() {
|
||||
console.error('Ошибка загрузки аватара:', this.avatarUrl);
|
||||
this.avatarLoadError = true;
|
||||
@@ -214,7 +295,7 @@ export default {
|
||||
this.avatarLoadError = false;
|
||||
try {
|
||||
await this.authStore.fetchProfile();
|
||||
await this.loadStats();
|
||||
await this.loadExtendedData();
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных:', error);
|
||||
} finally {
|
||||
@@ -222,29 +303,101 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
async loadExtendedData() {
|
||||
this.statsLoading = true;
|
||||
try {
|
||||
const [statsResult, achievementsResult] = await Promise.all([
|
||||
const [statsResult, achievementsResult, bestsResult, eventsResult, planResult] = await Promise.all([
|
||||
this.userStore.fetchUserStats(),
|
||||
this.userStore.fetchUserAchievements()
|
||||
this.userStore.fetchUserAchievements(),
|
||||
this.fetchPersonalBests(),
|
||||
this.fetchUpcomingEvents(),
|
||||
this.fetchCurrentTrainingPlan()
|
||||
]);
|
||||
|
||||
if (!statsResult.success) {
|
||||
console.error('Ошибка загрузки статистики:', statsResult.error);
|
||||
}
|
||||
if (!achievementsResult.success) {
|
||||
console.error('Ошибка загрузки достижений:', achievementsResult.error);
|
||||
}
|
||||
if (!statsResult.success) console.error('Ошибка статистики:', statsResult.error);
|
||||
if (!achievementsResult.success) console.error('Ошибка достижений:', achievementsResult.error);
|
||||
if (!bestsResult.success) console.error('Ошибка рекордов:', bestsResult.error);
|
||||
if (!eventsResult.success) console.error('Ошибка событий:', eventsResult.error);
|
||||
if (!planResult.success) console.error('Ошибка плана:', planResult.error);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки статистики:', error);
|
||||
console.error('Ошибка загрузки расширенных данных:', error);
|
||||
} finally {
|
||||
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() {
|
||||
await this.loadStats();
|
||||
await this.loadExtendedData();
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
@@ -257,21 +410,231 @@ export default {
|
||||
},
|
||||
|
||||
viewDetailedStats() {
|
||||
// TODO: Переход на страницу детальной статистики
|
||||
alert('Функция в разработке');
|
||||
this.$router.push('/stats/detailed');
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.user) {
|
||||
await this.loadUserData();
|
||||
} else {
|
||||
await this.loadStats();
|
||||
await this.loadExtendedData();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
Reference in New Issue
Block a user