532862c12b
modified: serv_nginx/api_bb/internal/service/avatar_service.go try save path
624 lines
15 KiB
Vue
624 lines
15 KiB
Vue
<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> |