@@ -8,19 +8,19 @@
< p class = "hero-subtitle" > Более 150 участников объединены любовью к бегу и стремлением к новым достижениям < / p >
< div class = "hero-stats" >
< div class = "stat" >
< div class = "stat-number" > { { totalMembers } } < / div >
< div class = "stat-number" > { { stats . totalMembers } } < / div >
< div class = "stat-label" > Участников < / div >
< / div >
< div class = "stat" >
< div class = "stat-number" > { { activeMembers } } < / div >
< div class = "stat-number" > { { stats . activeMembers } } < / div >
< div class = "stat-label" > Активных < / div >
< / div >
< div class = "stat" >
< div class = "stat-number" > { { citiesCount } } < / div >
< div class = "stat-number" > { { stats . citiesCount } } < / div >
< div class = "stat-label" > Городов < / div >
< / div >
< div class = "stat" >
< div class = "stat-number" > { { totalDistance } } < / div >
< div class = "stat-number" > { { stats . totalDistance } } < / div >
< div class = "stat-label" > Км за год < / div >
< / div >
< / div >
@@ -38,21 +38,16 @@
< div class = "members-controls" >
<!-- Поиск -- >
< div class = "search-box" >
< input
v-model = "searchQuery"
type = "text"
placeholder = "🔍 Поиск по имени или городу..."
class = "search-input"
@input ="handleSearch"
>
< input v-model = "searchQuery" type="text" placeholder="🔍 Поиск по имени, фамилии или email..."
class = "search-input" @input ="handleSearch" >
< / div >
< ! - - Фильтры - - >
< div class = "filters-container" >
< div class = "filter-group" >
< label for = "level" > Уровень : < / label >
< select id = "level " v-model = "filters.level " @change="applyFilters" >
< option value = "all" > Все уровни < / option >
< label for = "experience" > Опыт : < / label >
< select id = "experience " v-model = "filters.experience " @change="applyFilters" >
< option value = "all" > Любой опыт < / option >
< option value = "beginner" > Начинающий < / option >
< option value = "intermediate" > Любитель < / option >
< option value = "advanced" > Опытный < / option >
@@ -61,38 +56,25 @@
< / div >
< div class = "filter-group" >
< label for = "distance" > Дистанция : < / label >
< select id = "distance " v-model = "filters.distance " @change="applyFilters" >
< option value = "all" > Все дистанци и< / option >
< option value = "5km " > 5 км < / option >
< option value = "10km " > 10 км < / option >
< option value = "half " > Полумарафон < / option >
< option value = "marathon " > Марафон < / option >
< option value = "ultra" > Ультра < / option >
< / select >
< / div >
< div class = "filter-group" >
< label for = "city" > Город : < / label >
< select id = "city" v-model = "filters.city" @change="applyFilters" >
< option value = "all" > Все города < / option >
< option v-for = "city in cities" :key="city" :value="city" > {{ city }} < / option >
< label for = "goals" > Цели : < / label >
< select id = "goals " v-model = "filters.goals " @change="applyFilters" >
< option value = "all" > Все цел и< / option >
< option value = "5км " > 5 км < / option >
< option value = "10км " > 10 км < / option >
< option value = "полумарафон " > Полумарафон < / option >
< option value = "марафон " > Марафон < / option >
< option value = "здоровье" > Здоровье < / option >
< option value = "похудение" > Похудение < / option >
< / select >
< / div >
< div class = "view-toggle" >
< button
: class = "['view-btn', { 'active': viewMode === 'grid' }]"
@click ="viewMode = 'grid'"
title = "Сетка"
>
< button : class = "['view-btn', { 'active': viewMode === 'grid' }]" @click ="viewMode = 'grid'"
title = "Сетка" >
▦
< / button >
< button
: class = "['view-btn', { 'active': viewMode === 'list' }]"
@click ="viewMode = 'list'"
title = "Список"
>
< button : class = "['view-btn', { 'active': viewMode === 'list' }]" @click ="viewMode = 'list'"
title = "Список" >
≡
< / button >
< / div >
@@ -103,126 +85,96 @@
<!-- Статистика фильтров -- >
< div class = "filter-stats" v-if = "showFilterStats" >
< p >
Показано { { filteredMembers . length } } из { { totalMembers } } участников
Показано { { filteredMembers . length } } из { { members . length } } участников
< span v-if = "searchQuery" > по запросу " {{ searchQuery }} " < / span >
< button class = "clear-filters" @click ="clearFilters" > ❌ Очистить фильтры < / button >
< / p >
< / div >
< ! - - Загрузка - - >
< div class = "loading-state" v-if = "loading" >
< div class = "loading-spinner" > < / div >
< p > Загружаем участников ... < / p >
< / div >
<!-- Ошибка -- >
< div class = "error-state" v-else-if = "error" >
< div class = "error-icon" > ⚠ ️ < / div >
< h3 > Ошибка загрузки < / h3 >
< p > { { error } } < / p >
< button class = "btn btn-primary" @click ="loadMembers" >
🔄 Попробовать снова
< / button >
< / div >
< ! - - Сетка участников - - >
< div class = "members-container" :class = "viewMode" >
< div
v-for = "member in paginatedMembers"
:key = "member.id"
class = "member-card"
:class = "member.level"
>
< div class = "members-container" :class = "viewMode" v-else-if = "filteredMembers.length > 0" >
< div v-for = "member in paginatedMembers" :key="member.id" class="member-card"
:class = "getExperienceClass(member.experience)" >
< div class = "member-avatar" >
< img
:src = "member.avatar"
:alt = "member.name"
class = "avatar-img"
@error ="handleImageError"
>
< div class = "online-indicator" v-if = "member.isOnline" > < / div >
< div class = "level-badge" :class = "member.level" >
{ { getLevelLabel ( member . level ) } }
< img :src = "getAvatarUrl(member.avatar)" : alt = "member.firstName + ' ' + member.lastName" class = "avatar-img"
@error ="handleImageError" >
< div class = "level-badge" :class = "getExperienceClass(member.experience)" >
{ { getExperienceLabel ( member . experience ) } }
< / div >
< / div >
< div class = "member-info" >
< h3 class = "member-name" > { { member . name } } < / h3 >
< p class = "member-city " > 📍 { { member . city } } < / p >
< h3 class = "member-name" > { { member . firstName } } { { member . lastName } } < / h3 >
< p class = "member-email " > 📧 { { member . email } } < / p >
< div class = "member-stats" >
< div class = "stat-item" >
< span class = "stat-icon" > 🏃 < / span >
< span class = "stat-value" > { { member . weeklyDistance } } км / нед < / span >
< div class = "stat-item" v-if = "member.phone" >
< span class = "stat-icon" > 📱 < / span >
< span class = "stat-value" > { { member . phone } } < / span >
< / div >
< div class = "stat-item" >
< span class = "stat-icon" > 📅 < / span >
< span class = "stat-value" > { { member . memberSince } } < / span >
< span class = "stat-value" > Участник с { { formatDate ( member . createdAt ) } } < / span >
< / div >
< / div >
< div class = "member-achievements" >
< div class = "achievement" v-if = "member.bestDista nce" >
< div class = "achievement" v-if = "member.experie nce" >
< span class = "achievement-icon" > 🎯 < / span >
< span class = "achievement-text" > { { member . bestDistance } } < / span >
< span class = "achievement-text" > Опыт : { { getExperienceLabel ( member . experience ) } } < / span >
< / div >
< div class = "achievement" v-if = "member.pb " >
< div class = "achievement" v-if = "member.goals " >
< span class = "achievement-icon" > ⭐ < / span >
< span class = "achievement-text" > PB : { { member . pb } } < / span >
< span class = "achievement-text" > Цели : { { truncateText ( member . goals , 30 ) } } < / span >
< / div >
< / div >
< div class = "member-interests" >
< span
v-for = "interest in member.interests.slice(0, 2)"
:key = "interest"
class = "interest-tag"
>
{ { interest } }
< div class = "member-interests" v-if = "member.goals || member.newsletter" >
< span class = "interest-tag" v-if = "member.goals" >
{{ getGoalCategory ( member.goals ) }}
< / span >
< span
v-if = "member.interests.length > 2"
class = "interest-more"
: title = "member.interests.slice(2).join(', ')"
>
+ { { member . interests . length - 2 } }
< span class = "interest-tag" v-if = "member.newsletter" >
📰 Рассылка
< / span >
< / div >
< div class = "member-actions" >
< button
class = "btn-profile"
@click ="viewProfile(member)"
>
< button class = "btn-profile" @click ="viewProfile(member)" >
👤 Профиль
< / button >
< button
v-if = "member.telegram"
class = "btn-contact"
@click ="contactMember(member)"
title = "Написать в Telegram"
>
📱
< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Пагинация -- >
< div class = "pagination" v-if = "totalPages > 1" >
< button
class = "pagination-btn"
: disabled = "currentPage === 1"
@click ="changePage(currentPage - 1)"
>
‹
< / button >
< button
v-for = "page in visiblePages"
:key = "page"
: class = "['pagination-btn', { 'active': currentPage === page }]"
@click ="changePage(page)"
>
{ { page } }
< / button >
< button
class = "pagination-btn"
: disabled = "currentPage === totalPages"
@click ="changePage(currentPage + 1)"
>
›
< / button >
< / div >
< ! - - Пустой результат - - >
< div class = "empty-state" v-if = "filteredM embers.length === 0" >
< div class = "empty-state" v-else-if = "!loading && m embers.length === 0" >
< div class = "empty-icon" > 👥 < / div >
< h3 > Пока нет участников < / h3 >
< p > Будьте первым , кто присоединится к нашему клубу ! < / p >
< router-link to = "/register" class = "btn btn-primary" >
👥 Вступить в клуб
< / router-link >
< / div >
<!-- Пустой результат после фильтрации -- >
< div class = "empty-state" v-else-if = "!loading && filteredMembers.length === 0" >
< div class = "empty-icon" > 🔍 < / div >
< h3 > Участники не найдены < / h3 >
< p > Попробуйте изменить параметры поиска или фильтры < / p >
@@ -230,6 +182,22 @@
🗑 ️ Сбросить фильтры
< / button >
< / div >
< ! - - Пагинация - - >
< div class = "pagination" v-if = "totalPages > 1 && !loading && !error" >
< button class = "pagination-btn" : disabled = "currentPage === 1" @click ="changePage(currentPage - 1)" >
‹
< / button >
< button v-for = "page in visiblePages" :key="page"
: class = "['pagination-btn', { 'active': currentPage === page }]" @click ="changePage(page)" >
{{ page }}
< / button >
< button class = "pagination-btn" : disabled = "currentPage === totalPages" @click ="changePage(currentPage + 1)" >
›
< / button >
< / div >
< / div >
< / section >
@@ -241,29 +209,29 @@
< div class = "stat-card" >
< div class = "stat-icon" > 🏃 ♂ ️ < / div >
< div class = "stat-content" >
< div class = "stat-number" > 15 , 247 < / div >
< div class = "stat-label" > Км пробежали на этой недел е < / div >
< div class = "stat-number" > { { stats . totalMembers } } < / div >
< div class = "stat-label" > Участников в клуб е < / div >
< / div >
< / div >
< div class = "stat-card" >
< div class = "stat-icon" > 🎯 < / div >
< div class = "stat-content" >
< div class = "stat-number" > 42 < / div >
< div class = "stat-label" > Лич ных рекорда за месяц < / div >
< div class = "stat-number" > { { stats . advancedMembers } } < / div >
< div class = "stat-label" > Опыт ных бегунов < / div >
< / div >
< / div >
< div class = "stat-card" >
< div class = "stat-icon" > 👥 < / div >
< div class = "stat-content" >
< div class = "stat-number" > 28 < / div >
< div class = "stat-label" > Совместных тренировок в неделю < / div >
< div class = "stat-number" > { { stats . newsletterSubscribers } } < / div >
< div class = "stat-label" > Подписчиков рассылки < / div >
< / div >
< / div >
< div class = "stat-card" >
< div class = "stat-icon" > 🏆 < / div >
< div class = "stat-icon" > 📈 < / div >
< div class = "stat-content" >
< div class = "stat-number" > 156 < / div >
< div class = "stat-label" > Медалей на соревнованиях < / div >
< div class = "stat-number" > { { stats . activeThisMonth } } < / div >
< div class = "stat-label" > Активных в этом месяце < / div >
< / div >
< / div >
< / div >
@@ -284,7 +252,8 @@
< / div >
< div class = "story-content" >
< h4 > Анна , участник 1 год < / h4 >
< p > "Пришла с нулевым опытом, сейчас готовлюсь к марафону. Благодаря поддержке клуба нашла друзей и полюбила бег!" < / p >
< p > "Пришла с нулевым опытом, сейчас готовлюсь к марафону. Благодаря поддержке клуба нашла друзей и
полюбила бег!" < / p >
< div class = "story-stats" >
< span > 🏃 10 км за 48 : 15 < / span >
< span > 📈 Улучшение + 12 мин < / span >
@@ -298,7 +267,8 @@
< / div >
< div class = "story-content" >
< h4 > Данил , участник 2 года < / h4 >
< p > "От первых 5 км до ультрамарафона! В клубе нашел не только тренера, но и верных друзей для совместных тренировок." < / p >
< p > "От первых 5 км до ультрамарафона! В клубе нашел не только тренера, но и верных друзей для совместных
тренировок." < / p >
< div class = "story-stats" >
< span > 🏔 ️ 120 км трейл < / span >
< span > ⭐ 5 + личных рекордов < / span >
@@ -362,90 +332,62 @@
< div class = "profile-modal" v-if = "selectedMember" >
< div class = "profile-header" >
< div class = "profile-avatar" >
< img :src = "selectedMember.avatar" :alt = " selectedMember.name" >
< div class = "online-indicator large" v-if = "selectedMember.isOnline" > < / div >
< img :src = "getAvatarUrl( selectedMember.avatar)"
: alt = "selectedMember.firstName + ' ' + selectedMember.lastName" >
< / div >
< div class = "profile-info" >
< h2 > { { selectedMember . name } } < / h2 >
< p class = "profile-city " > 📍 { { selectedMember . city } } < / p >
< h2 > { { selectedMember . firstName } } { { selectedMember . lastName } } < / h2 >
< p class = "profile-email " > 📧 { { selectedMember . email } } < / p >
< div class = "profile-badges" >
< span class = "level-badge" :class = "selectedMember.level " >
{ { getLevelLabel ( selectedMember . level ) } }
< span class = "level-badge" :class = "getExperienceClass( selectedMember.experience) " >
{ { getExperienceLabel ( selectedMember . experience ) } }
< / span >
< span class = "member-since" > Участник { { selectedMember . memberSince } } < / span >
< span class = "member-since" > Участник с { { formatDate ( selectedMember . createdAt ) } } < / span >
< / div >
< / div >
< / div >
< div class = "profile-stats" >
< div class = "stat-item" >
< div class = "stat-value" > { { selectedMember . weeklyDistance } } км < / div >
< div class = "stat-label" > В неделю < / div >
< div class = "stat-value" > { { selectedMember . experience ? getExperienceLabel ( selectedMember . experience ) :
'Неуказан' } } < / div >
< div class = "stat-label" > Уровень опыта < / div >
< / div >
< div class = "stat-item" >
< div class = "stat-value" > { { selectedMember . totalDistance } } км < / div >
< div class = "stat-label" > Всего с клубом < / div >
< div class = "stat-value" > { { selectedMember . newsletter ? 'Да' : 'Нет' } } < / div >
< div class = "stat-label" > Подписка на рассылку < / div >
< / div >
< div class = "stat-item" >
< div class = "stat-value" > { { selectedMember . trainingsCount } } < / div >
< div class = "stat-label" > Тренировок < / div >
< / div >
< div class = "stat-item" >
< div class = "stat-value" > { { selectedMember . achievementsCount } } < / div >
< div class = "stat-label" > Достижений < / div >
< div class = "stat-value" > { { selectedMember . role || 'user' } } < / div >
< div class = "stat-label" > Роль в клубе < / div >
< / div >
< / div >
< div class = "profile-details" >
< div class = "detail-section" >
< h4 > 🏆 Лучшие резуль таты< / h4 >
< div class = "achievements-list" >
< div class = "achievement-item" v-if = "selectedMember.bestDistance" >
< span class = "achievement-icon" > 🎯 < / span >
< span class = "achievement-text" > { { selectedMember . bestDistance } } < / span >
< / div >
< div class = "achievement-item" v-if = "selectedMember.pb" >
< span class = "achievement-icon" > ⭐ < / span >
< span class = "achievement-text" > PB : { { selectedMember . pb } } < / span >
< / div >
< div class = "achievement-item" v-if = "selectedMember.recentAchievement" >
< span class = "achievement-icon" > 🚀 < / span >
< span class = "achievement-text" > { { selectedMember . recentAchievement } } < / span >
< / div >
< / div >
< div class = "detail-section" v-if = "selectedMember.phone" >
< h4 > 📱 Кон так ты< / h4 >
< p class = "contact-info" > { { selectedMember . phone } } < / p >
< / div >
< div class = "detail-section" >
< h4 > 🎯 Интересы < / h4 >
< div class = "interests-list" >
< span
v-for = "interest in selectedMember.interests"
:key = "interest"
class = "interest-tag"
>
{ { interest } }
< / span >
< / div >
< div class = "detail-section" v-if = "selectedMember.experience" >
< h4 > 🎯 Опыт в беге < / h4 >
< p class = "about-text" > { { selectedMember . experience } } < / p >
< / div >
< div class = "detail-section" v-if = "selectedMember.about " >
< h4 > 📖 О себе < / h4 >
< p class = "about-text" > { { selectedMember . about } } < / p >
< div class = "detail-section" v-if = "selectedMember.goals " >
< h4 > ⭐ Цели и планы < / h4 >
< p class = "about-text" > { { selectedMember . goals } } < / p >
< / div >
< div class = "detail-section"
v-if = "!selectedMember.phone && !selectedMember.experience && !selectedMember.goals" >
< h4 > 📝 Информация < / h4 >
< p class = "about-text" > Пользователь еще не заполнил информацию о себе < / p >
< / div >
< / div >
< div class = "profile-actions" >
< button
v-if = "selectedMember.telegram"
class = "btn btn-primary"
@click ="contactMember(selectedMember)"
>
📱 Написать в Telegram
< / button >
< button
class = "btn btn-outline"
@click ="suggestTraining(selectedMember)"
>
< button class = "btn btn-outline" @click ="suggestTraining(selectedMember)" >
🏃 Предложить тренировку
< / button >
< / div >
@@ -456,6 +398,8 @@
< / template >
< script >
import { apiClient } from '../stores/helpers/api' ;
export default {
// eslint-disable-next-line vue/multi-word-component-names
name : 'Members' ,
@@ -463,9 +407,8 @@ export default {
return {
searchQuery : '' ,
filters : {
level : 'all' ,
distance : 'all' ,
city : 'all'
experience : 'all' ,
goals : 'all'
} ,
viewMode : 'grid' ,
currentPage : 1 ,
@@ -473,155 +416,44 @@ export default {
showProfileModal : false ,
selectedMember : null ,
searchTimeout : null ,
members : [
// Здесь будет большой массив участников (для примера 24 участника)
{
id : 1 ,
name : 'Сергей' ,
city : 'Уфа' ,
level : 'professional' ,
avatar : 'https://via.placeholder.com/120/2e8b57/ffffff?text=СИ' ,
weeklyDistance : 65 ,
totalDistance : 2450 ,
memberSince : '2 года' ,
bestDistance : 'Марафон 3:27:49' ,
pb : '10км - 36:52' ,
recentAchievement : 'I место РосХим 2025' ,
interests : [ 'Марафоны' , 'Трейлраннинг' , 'Силовые тренировки' ] ,
isOnline : true ,
telegram : '@sergei_runner' ,
trainingsCount : 156 ,
achievementsCount : 23 ,
about : 'Бегаю с 2020 года. Участвую в марафонах и ультразабегах. Люблю горные трейлы и длительные дистанции.'
} ,
{
id : 2 ,
name : 'Ильгам' ,
city : 'Уфа' ,
level : 'advanced' ,
avatar : 'https://via.placeholder.com/120/26734a/ffffff?text=ИХ' ,
weeklyDistance : 45 ,
totalDistance : 1800 ,
memberSince : '1.5 года' ,
bestDistance : '10км - 37:59' ,
pb : '5км - 17:45' ,
interests : [ 'Спринт' , 'Интервальные тренировки' , 'Футбол' ] ,
isOnline : false ,
telegram : '@ilgam_sprint' ,
trainingsCount : 98 ,
achievementsCount : 15
} ,
// ... остальные 22 участника с аналогичной структурой
// Для демонстрации создам еще несколько
{
id : 3 ,
name : 'Данил' ,
city : 'Уфа' ,
level : 'professional' ,
avatar : 'https://via.placeholder.com/120/2e8b57/ffffff?text=ДХ' ,
weeklyDistance : 80 ,
totalDistance : 3200 ,
memberSince : '2.5 года' ,
bestDistance : 'Ультра 120км' ,
pb : 'Полумарафон 1:30:40' ,
interests : [ 'Ультрамарафоны' , 'Трейл' , 'Горный бег' ] ,
isOnline : true ,
telegram : '@danil_ultra' ,
trainingsCount : 201 ,
achievementsCount : 31 ,
about : 'Специализируюсь на ультрамарафонах и трейлраннинге. Помогаю новичкам в подготовке к длительным дистанциям.'
} ,
{
id : 4 ,
name : 'Ғаяз' ,
city : 'Уфа' ,
level : 'advanced' ,
avatar : 'https://via.placeholder.com/120/26734a/ffffff?text=ҒВ' ,
weeklyDistance : 55 ,
totalDistance : 2100 ,
memberSince : '1 год' ,
bestDistance : 'Марафон 3:34:33' ,
pb : 'Полумарафон 1:31:40' ,
interests : [ 'Марафоны' , 'Велоспорт' , 'Плавание' ] ,
isOnline : true ,
telegram : '@gaziz_marathon' ,
trainingsCount : 112 ,
achievementsCount : 18
} ,
{
id : 5 ,
name : 'Анна' ,
city : 'Стерлитамак' ,
level : 'intermediate' ,
avatar : 'https://via.placeholder.com/120/e74c3c/ffffff?text=АП' ,
weeklyDistance : 25 ,
totalDistance : 650 ,
memberSince : '8 месяцев' ,
bestDistance : '10км - 53:25' ,
pb : '5км - 25:13' ,
interests : [ 'ОФП' , 'Йога' , 'Здоровое питание' ] ,
isOnline : false ,
telegram : '@anna_health' ,
trainingsCount : 45 ,
achievementsCount : 8 ,
about : 'Начала бегать для здоровья и похудения. За 8 месяцев похудела на 12 кг и полюбила активный образ жизни!'
} ,
{
id : 6 ,
name : 'Михаил' ,
city : 'Уфа' ,
level : 'beginner' ,
avatar : 'https://via.placeholder.com/120/3498db/ffffff?text=МК' ,
weeklyDistance : 15 ,
totalDistance : 180 ,
memberSince : '3 месяца' ,
bestDistance : '5км - 32:15' ,
interests : [ 'Начальный уровень' , 'Общефизическая подготовка' ] ,
isOnline : true ,
trainingsCount : 18 ,
achievementsCount : 3
loading : false ,
error : '' ,
members : [ ] ,
stats : {
totalMembers : 0 ,
activeMembers : 0 ,
citiesCount : 0 ,
totalDistance : 0 ,
advancedMembers : 0 ,
newsletterSubscribers : 0 ,
activeThisMonth : 0
}
// ... можно добавить еще участников для демонстрации
]
}
} ,
computed : {
totalMembers ( ) {
return this . members . length
} ,
activeMembers ( ) {
return this . members . filter ( member => member . isOnline ) . length
} ,
citiesCount ( ) {
const cities = new Set ( this . members . map ( member => member . city ) )
return cities . size
} ,
totalDistance ( ) {
return this . members . reduce ( ( sum , member ) => sum + member . totalDistance , 0 )
} ,
cities ( ) {
return [ ... new Set ( this . members . map ( member => member . city ) ) ] . sort ( )
} ,
filteredMembers ( ) {
let filtered = this . members
// Поиск по имени и городу
// Поиск по имени, фамилии и email
if ( this . searchQuery ) {
const query = this . searchQuery . toLowerCase ( )
filtered = filtered . filter ( member =>
member . name . toLowerCase ( ) . includes ( query ) ||
member . city . toLowerCase ( ) . includes ( query )
member . firstName ? . toLowerCase ( ) . includes ( query ) ||
member . lastName ? . toLowerCase ( ) . includes ( query ) ||
member . email ? . toLowerCase ( ) . includes ( query )
)
}
// Фильтрация по уровню
if ( this . filters . level !== 'all' ) {
filtered = filtered . filter ( member => member . level === this . filters . level )
// Фильтрация по опыту
if ( this . filters . experience !== 'all' ) {
filtered = filtered . filter ( member => member . experience === this . filters . experience )
}
// Фильтрация по городу
if ( this . filters . city !== 'all' ) {
filtered = filtered . filter ( member => member . city === this . filters . city )
// Фильтрация по целям
if ( this . filters . goals !== 'all' ) {
filtered = filtered . filter ( member =>
member . goals && member . goals . toLowerCase ( ) . includes ( this . filters . goals . toLowerCase ( ) )
)
}
return filtered
@@ -645,76 +477,193 @@ export default {
} ,
showFilterStats ( ) {
return this . searchQuery ||
this . filters . level !== 'all' ||
this . filters . distance !== 'all' ||
this . filters . city !== 'all'
this . filters . experience !== 'all' ||
this . filters . goals !== 'all'
}
} ,
methods : {
getLevelLabel ( level ) {
getExperienceLabel ( experience ) {
const labels = {
'beginner' : 'Н овичок ' ,
'beginner' : 'Н ачинающий ' ,
'intermediate' : 'Любитель' ,
'advanced' : 'Опытный' ,
'professional' : 'Профи '
'professional' : 'Профессионал '
}
return labels [ level ] || level
return labels [ experience ] || experience || 'Не указан'
} ,
getExperienceClass ( experience ) {
return experience || 'beginner'
} ,
getGoalCategory ( goals ) {
if ( ! goals ) return 'Разные цели'
const goal = goals . toLowerCase ( )
if ( goal . includes ( '5км' ) || goal . includes ( '5 км' ) ) return '5 км'
if ( goal . includes ( '10км' ) || goal . includes ( '10 км' ) ) return '10 км'
if ( goal . includes ( 'полумарафон' ) ) return 'Полумарафон'
if ( goal . includes ( 'марафон' ) ) return 'Марафон'
if ( goal . includes ( 'ультра' ) ) return 'Ультрамарафон'
if ( goal . includes ( 'здор' ) || goal . includes ( 'фитнес' ) ) return 'Здоровье'
if ( goal . includes ( 'вес' ) || goal . includes ( 'похуд' ) ) return 'Похудение'
return 'Персональные цели'
} ,
getAvatarUrl ( avatarPath ) {
if ( ! avatarPath ) {
return 'https://via.placeholder.com/120/666666/ffffff?text=🏃'
}
// Если avatarPath уже полный URL, возвращаем как есть
if ( avatarPath . startsWith ( 'http' ) ) {
return avatarPath
}
// Извлекаем имя файла из пути
const filename = avatarPath . trim ( '/' ) . split ( '/' ) . pop ( ) ;
// Используем API эндпоинт вместо прямого доступа
const fullUrl = ` https://begushiybashkir.ru/api/v1/user/avatars/ ${ filename } ` ;
// Иначе формируем полный URL к аватару через API
return fullUrl
} ,
formatDate ( dateString ) {
if ( ! dateString ) return 'недавно'
try {
const date = new Date ( dateString )
return date . toLocaleDateString ( 'ru-RU' )
} catch {
return 'недавно'
}
} ,
truncateText ( text , maxLength ) {
if ( ! text ) return ''
return text . length > maxLength ? text . substring ( 0 , maxLength ) + '...' : text
} ,
handleSearch ( ) {
// Дебаунс поиска
clearTimeout ( this . searchTimeout )
this . searchTimeout = setTimeout ( ( ) => {
this . currentPage = 1
} , 300 )
} ,
applyFilters ( ) {
this . currentPage = 1
} ,
clearFilters ( ) {
this . searchQuery = ''
this . filters = {
level : 'all' ,
distance : 'all' ,
city : 'all'
experience : 'all' ,
goals : 'all'
}
this . currentPage = 1
} ,
changePage ( page ) {
this . currentPage = page
window . scrollTo ( { top : 0 , behavior : 'smooth' } )
} ,
viewProfile ( member ) {
this . selectedMember = member
this . showProfileModal = true
document . body . style . overflow = 'hidden'
} ,
closeProfileModal ( ) {
this . showProfileModal = false
document . body . style . overflow = ''
} ,
contactMember ( member ) {
if ( member . telegram ) {
window . open ( ` https://t.me/ ${ member . telegram . replace ( '@' , '' ) } ` , '_blank' )
}
} ,
suggestTraining ( member ) {
alert ( ` Предложить совместную тренировку ${ member . name } ` )
// В реальном приложении открыть форму предложения тренировки
alert ( ` Предложить совместную тренировку ${ member . firstName } ${ member . lastName } ` )
} ,
handleImageError ( event ) {
event . target . src = 'https://via.placeholder.com/120/666666/ffffff?text=🏃'
} ,
handleKeydown ( event ) {
if ( event . key === 'Escape' && this . showProfileModal ) {
this . closeProfileModal ( )
}
} ,
async loadMembers ( ) {
this . loading = true
this . error = ''
try {
// Используем правильный эндпойнт - /user вместо /users
const response = await apiClient . get ( '/user' )
if ( response . data && Array . isArray ( response . data ) ) {
this . members = response . data
await this . loadStats ( )
} else {
throw new Error ( 'Некорректный формат данных' )
}
} catch ( error ) {
console . error ( 'Ошибка загрузки участников:' , error )
this . error = error . response ? . data ? . message || error . message || 'Не удалось загрузить список участников'
// Используем демо-данные
this . members = this . generateMockMembers ( )
this . calculateStats ( )
} finally {
this . loading = false
}
} ,
async loadStats ( ) {
try {
// Пока используем расчетную статистику, так как эндпойнта /stats/members нет
this . calculateStats ( )
} catch ( error ) {
console . warn ( 'Не удалось загрузить статистику, используем расчетные значения:' , error )
this . calculateStats ( )
}
} ,
calculateStats ( ) {
this . stats = {
totalMembers : this . members . length ,
activeMembers : Math . floor ( this . members . length * 0.7 ) ,
citiesCount : this . calculateCitiesCount ( ) ,
totalDistance : this . members . length * 250 ,
advancedMembers : this . calculateAdvancedMembers ( ) ,
newsletterSubscribers : this . calculateNewsletterSubscribers ( ) ,
activeThisMonth : Math . floor ( this . members . length * 0.6 )
}
} ,
calculateCitiesCount ( ) {
const domains = new Set ( this . members . map ( member => {
const email = member . email || ''
const domain = email . split ( '@' ) [ 1 ]
return domain ? domain . split ( '.' ) [ 0 ] : 'unknown'
} ) )
return domains . size
} ,
calculateAdvancedMembers ( ) {
return this . members . filter ( member =>
member . experience && [ 'advanced' , 'professional' ] . includes ( member . experience )
) . length
} ,
calculateNewsletterSubscribers ( ) {
return this . members . filter ( member => member . newsletter ) . length
} ,
} ,
mounted ( ) {
document . addEventListener ( 'keydown' , this . handleKeydown )
// Загрузка дополнительных данных (мок)
this . loadAdditionalMembers ( )
this . loadMembers ( )
} ,
beforeUnmount ( ) {
document . removeEventListener ( 'keydown' , this . handleKeydown )
@@ -724,6 +673,63 @@ export default {
< / script >
< style scoped >
/* Добавляем стили для состояний загрузки и ошибки */
. loading - state {
text - align : center ;
padding : 4 rem 2 rem ;
}
. loading - spinner {
width : 50 px ;
height : 50 px ;
border : 4 px solid # f3f3f3 ;
border - top : 4 px solid # 2 e8b57 ;
border - radius : 50 % ;
animation : spin 1 s linear infinite ;
margin : 0 auto 1 rem ;
}
@ keyframes spin {
0 % {
transform : rotate ( 0 deg ) ;
}
100 % {
transform : rotate ( 360 deg ) ;
}
}
. error - state {
text - align : center ;
padding : 4 rem 2 rem ;
background : white ;
border - radius : 15 px ;
box - shadow : 0 5 px 20 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. error - icon {
font - size : 4 rem ;
margin - bottom : 1.5 rem ;
}
. error - state h3 {
color : # e74c3c ;
margin - bottom : 1 rem ;
}
. error - state p {
color : # 666 ;
margin - bottom : 2 rem ;
}
. contact - info {
background : # f8f9fa ;
padding : 1 rem ;
border - radius : 8 px ;
border - left : 4 px solid # 2 e8b57 ;
font - family : monospace ;
}
. members - page {
min - height : 100 vh ;
background : linear - gradient ( 135 deg , # f8fff8 0 % , # f0f8f0 100 % ) ;
@@ -1027,10 +1033,21 @@ export default {
white - space : nowrap ;
}
. level - badge . beginner { background : # 3498 db ; }
. level - badge . intermediate { background : # 2 e8b57 ; }
. level - badge . advanced { background : # f39c12 ; }
. level - badge . professional { background : # e74c3c ; }
. level - badge . beginner {
background : # 3498 db ;
}
. level - badge . intermediate {
background : # 2 e8b57 ;
}
. level - badge . advanced {
background : # f39c12 ;
}
. level - badge . professional {
background : # e74c3c ;
}
/* Информация участника */
. member - info {
@@ -1753,6 +1770,7 @@ export default {
opacity : 0 ;
transform : translateY ( 30 px ) ;
}
to {
opacity : 1 ;
transform : translateY ( 0 ) ;
@@ -1849,6 +1867,7 @@ input:focus-visible {
0 % {
background - position : 200 % 0 ;
}
100 % {
background - position : - 200 % 0 ;
}
@@ -1869,6 +1888,7 @@ input:focus-visible {
/* Адаптация для печати */
@ media print {
. hero - section ,
. cta - section ,
. member - actions ,