Files
tp/begushiybashkir/bbvue/src/views/Profile.vue
T
valitovgaziz 532862c12b modified: begushiybashkir/bbvue/src/views/Profile.vue
modified:   serv_nginx/api_bb/internal/service/avatar_service.go
try save path
2025-10-14 22:58:18 +05:00

624 lines
15 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page">
<h1>👤 Личный кабинет</h1>
<div v-if="authLoading" class="loading">Загрузка профиля...</div>
<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"
>
<div v-else class="avatar-placeholder">
👤
</div>
</div>
<AvatarUpload :user="user" @avatar-updated="onAvatarUpdated" />
</div>
<h2>{{ user.firstName }} {{ user.lastName }}</h2>
<p>Участник с {{ joinDate }}</p>
<p class="user-email">{{ user.email }}</p>
<p v-if="user.phone" class="user-phone">📱 {{ user.phone }}</p>
</div>
<!-- Остальной код остается без изменений -->
<div class="profile-info">
<h3>📋 Информация о пользователе</h3>
<div class="info-grid">
<div class="info-item">
<label>Уровень подготовки:</label>
<span class="info-value">{{ experienceLabel }}</span>
</div>
<div class="info-item">
<label>Цели:</label>
<span class="info-value">{{ goalsLabel }}</span>
</div>
<div class="info-item">
<label>Рассылка:</label>
<span class="info-value">{{ user.newsletter ? '✅ Подключена' : '❌ Отключена' }}</span>
</div>
<div class="info-item">
<label>Роль:</label>
<span class="info-value role-badge">{{ user.role }}</span>
</div>
</div>
</div>
<div class="profile-stats">
<div class="stats-header">
<h3>📊 Моя статистика</h3>
<button class="btn-refresh" @click="refreshStats" :disabled="statsLoading">
{{ statsLoading ? '⟳' : '🔄' }}
</button>
</div>
<div v-if="statsError" class="error-message">
{{ statsError }}
</div>
<div v-else class="stats-grid">
<div class="stat-card">
<h4>🏃 Всего пробег</h4>
<p>{{ userStats?.totalDistance || 0 }} км</p>
</div>
<div class="stat-card">
<h4> Лучший результат</h4>
<p>{{ userStats?.bestResult || 'Нет данных' }}</p>
</div>
<div class="stat-card">
<h4>📅 Тренировок</h4>
<p>{{ userStats?.totalWorkouts || 0 }}</p>
</div>
<div class="stat-card">
<h4>🔥 Сожжено калорий</h4>
<p>{{ userStats?.caloriesBurned || 0 }}</p>
</div>
</div>
</div>
<div class="achievements-preview">
<h3>🏆 Достижения</h3>
<div class="achievements-progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: achievementProgress + '%' }"></div>
</div>
<span>{{ achievementProgress }}% выполнено</span>
</div>
<div class="achievements-count">
<span>Получено: {{ completedAchievements.length }} из {{ userAchievements.length }}</span>
</div>
<button class="btn btn-outline" @click="$router.push('/achievements')">
📜 Все достижения
</button>
</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 btn-logout" @click="handleLogout" :disabled="authLoading">
{{ authLoading ? 'Выход...' : '🚪 Выйти' }}
</button>
</div>
</div>
<div v-else class="error-message">
Не удалось загрузить данные профиля.
<router-link to="/login" class="link">Войдите</router-link> снова.
</div>
<button class="btn btn-secondary" @click="$router.push('/')"> На главную</button>
</div>
</template>
<script>
import { useAuthStore } from '../stores/auth'
import { useUserStore } from '../stores/user'
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Profile',
setup() {
const authStore = useAuthStore()
const userStore = useUserStore()
return { authStore, userStore }
},
data() {
return {
authLoading: false,
statsLoading: false,
avatarLoadError: false
}
},
computed: {
user() {
return this.authStore.user
},
userStats() {
return this.userStore.userStats
},
userAchievements() {
return this.userStore.userAchievements
},
completedAchievements() {
return this.userStore.completedAchievements
},
achievementProgress() {
return this.userStore.achievementProgress
},
statsError() {
return this.userStore.error
},
// Вычисляем полный URL аватара
avatarUrl() {
if (!this.user?.avatar) return null;
// Если avatar уже содержит полный URL, возвращаем как есть
if (this.user.avatar.startsWith('http')) {
return this.user.avatar;
}
// Иначе формируем полный URL
const baseUrl = 'https://begushiybashkir.ru/api/v1/user';
alert(this.user.avatar)
return baseUrl + this.user.avatar;
},
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();
return `${month} ${year}`;
},
experienceLabel() {
const experienceMap = {
'beginner': 'Начинающий (0-6 месяцев)',
'intermediate': 'Любитель (6-24 месяцев)',
'advanced': 'Опытный (2+ лет)',
'professional': 'Профессионал'
};
return experienceMap[this.user?.experience] || 'Не указан';
},
goalsLabel() {
const goalsMap = {
'health': 'Улучшить здоровье',
'weight': 'Сбросить вес',
'first5k': 'Пробежать первые 5 км',
'first10k': 'Пробежать первые 10 км',
'halfMarathon': 'Подготовиться к полумарафону',
'marathon': 'Подготовиться к марафону',
'improve': 'Улучшить результаты',
'social': 'Общение и компания'
};
return goalsMap[this.user?.goals] || 'Не указана';
}
},
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;
},
async loadUserData() {
this.authLoading = true;
this.avatarLoadError = false;
try {
await this.authStore.fetchProfile();
await this.loadStats();
} catch (error) {
console.error('Ошибка загрузки данных:', error);
} finally {
this.authLoading = false;
}
},
async loadStats() {
this.statsLoading = true;
try {
const [statsResult, achievementsResult] = await Promise.all([
this.userStore.fetchUserStats(),
this.userStore.fetchUserAchievements()
]);
if (!statsResult.success) {
console.error('Ошибка загрузки статистики:', statsResult.error);
}
if (!achievementsResult.success) {
console.error('Ошибка загрузки достижений:', achievementsResult.error);
}
} catch (error) {
console.error('Ошибка загрузки статистики:', error);
} finally {
this.statsLoading = false;
}
},
async refreshStats() {
await this.loadStats();
},
async handleLogout() {
await this.authStore.logout();
this.$router.push('/');
},
editProfile() {
this.$router.push('/profile/edit');
},
viewDetailedStats() {
// TODO: Переход на страницу детальной статистики
alert('Функция в разработке');
}
},
async mounted() {
if (!this.user) {
await this.loadUserData();
} else {
await this.loadStats();
}
}
}
</script>
<style scoped>
.page {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.profile-content {
margin-top: 2rem;
}
.profile-header {
text-align: center;
margin-bottom: 3rem;
padding: 2rem;
background: white;
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.avatar-section {
margin-bottom: 1.5rem;
}
.avatar-preview {
width: 150px;
height: 150px;
margin: 0 auto 1rem;
border-radius: 50%;
overflow: hidden;
border: 4px solid #2e8b57;
background: linear-gradient(135deg, #f5f5f5, #e0e0e0);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.avatar-image:hover {
transform: scale(1.05);
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
background: linear-gradient(135deg, #2e8b57, #3cb371);
color: white;
}
.profile-header h2 {
margin: 1rem 0 0.5rem;
color: #333;
font-size: 1.8rem;
}
.user-email, .user-phone {
color: #666;
margin: 0.25rem 0;
}
.profile-info, .profile-stats, .achievements-preview {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #2e8b57;
}
.info-item label {
font-weight: 600;
color: #555;
}
.info-value {
color: #333;
font-weight: 500;
}
.role-badge {
background: #2e8b57;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.85rem;
}
.stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.btn-refresh {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: all 0.3s ease;
background-color: #f8f9fa;
}
.btn-refresh:hover:not(:disabled) {
background-color: #2e8b57;
color: white;
transform: rotate(90deg);
}
.btn-refresh:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.stat-card {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
padding: 1.5rem 1rem;
border-radius: 12px;
text-align: center;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
border-color: #2e8b57;
}
.stat-card h4 {
margin: 0 0 0.5rem;
color: #555;
font-size: 0.9rem;
}
.stat-card p {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
color: #2e8b57;
}
.achievements-preview {
text-align: center;
}
.achievements-progress {
display: flex;
align-items: center;
gap: 1rem;
margin: 1rem 0;
justify-content: center;
}
.progress-bar {
flex: 1;
max-width: 300px;
height: 12px;
background-color: #e0e0e0;
border-radius: 6px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #2e8b57, #3cb371);
transition: width 0.5s ease;
}
.achievements-count {
margin: 1rem 0;
color: #666;
font-weight: 500;
}
.profile-actions {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 300px;
margin: 2rem auto;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: #2e8b57;
color: white;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn:hover:not(:disabled) {
background: #26734d;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(46, 139, 87, 0.3);
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-outline {
background: white;
color: #2e8b57;
border: 2px solid #2e8b57;
}
.btn-outline:hover {
background: #2e8b57;
color: white;
}
.btn-logout {
background-color: #dc3545;
margin-top: 1rem;
}
.btn-logout:hover:not(:disabled) {
background-color: #c82333;
}
.btn-secondary {
background-color: #6c757d;
margin-top: 2rem;
}
.btn-secondary:hover {
background-color: #545b62;
}
.error-message {
background-color: #fee;
color: #c33;
padding: 2rem;
border-radius: 8px;
text-align: center;
margin: 2rem 0;
border-left: 4px solid #c33;
}
.loading {
text-align: center;
padding: 2rem;
font-size: 1.1rem;
color: #666;
}
.link {
color: #2e8b57;
text-decoration: none;
font-weight: 600;
}
.link:hover {
text-decoration: underline;
}
/* Адаптивность */
@media (max-width: 768px) {
.page {
padding: 1rem;
}
.info-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.profile-actions {
max-width: 100%;
}
.achievements-progress {
flex-direction: column;
gap: 0.5rem;
}
.avatar-preview {
width: 120px;
height: 120px;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.profile-header {
padding: 1.5rem;
}
.profile-header h2 {
font-size: 1.5rem;
}
}
</style>