Files
akmon/pages/sport/student/favorite-exercises.uvue
2026-01-20 08:04:15 +08:00

1293 lines
30 KiB
Plaintext

<template>
<scroll-view direction="vertical" class="favorite-exercises-page" :scroll-y="true" :enable-back-to-top="true">
<!-- Header -->
<view class="header">
<view class="header-left">
<button @click="goBack" class="back-btn">
<simple-icon type="arrow-left" :size="16" color="#FFFFFF" />
<text>返回</text>
</button>
<text class="title">喜欢的运动</text>
</view>
<view class="header-actions">
<button @click="showSearch = !showSearch" class="search-btn">
<simple-icon type="search" :size="16" color="#FFFFFF" />
<text>搜索</text>
</button>
</view>
</view>
<!-- Search Bar -->
<view v-if="showSearch" class="search-bar">
<input :value="searchQuery" class="search-input"
placeholder="搜索运动项目..." @input="onSearchInput" />
<button v-if="searchQuery" @click="clearSearch" class="clear-btn">
<simple-icon type="x" :size="16" color="#999" />
</button>
</view>
<!-- Loading State -->
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<!-- Content -->
<scroll-view v-else class="content" scroll-y="true" :style="{ height: contentHeight + 'px' }">
<!-- User Preferences Summary -->
<view class="summary-section">
<view class="section-title">我的运动偏好</view>
<view class="summary-stats">
<view class="stat-item">
<text class="stat-number">{{ favoriteCount }}</text>
<text class="stat-label">喜爱运动</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ totalWeeklyTime }}</text>
<text class="stat-label">周计划时长(分钟)</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ averageIntensity }}</text>
<text class="stat-label">平均强度</text>
</view>
</view>
</view>
<!-- Category Tabs -->
<view class="category-section">
<view class="section-title">运动分类</view>
<scroll-view class="category-tabs" scroll-x="true">
<view v-for="category in categories" :key="category.key"
class="category-tab"
:class="{ 'active': selectedCategory == category.key }"
@click="selectCategory(category.key.toString())">
<text class="category-emoji">{{ category.icon }}</text>
<text class="category-name">{{ category.name }}</text>
</view>
</scroll-view>
</view>
<!-- Sports List -->
<view class="sports-section">
<view class="section-title">{{ getCategoryName(selectedCategory) }}</view>
<view v-if="filteredSports.length === 0" class="empty-state">
<simple-icon type="search" :size="48" color="#BDC3C7" />
<text class="empty-text">没有找到相关运动</text>
<text class="empty-desc">试试其他搜索词或选择不同分类</text>
</view>
<view v-else class="sports-list">
<view v-for="sport in (filteredSports as UTSJSONObject[])" :key="sport.id"
class="sport-item" @click="toggleSportPreference(sport)">
<view class="sport-header">
<view class="sport-icon">
<text class="sport-emoji">{{ getSportIcon(sport) }}</text>
</view>
<view class="sport-info">
<text class="sport-name">{{ sport.name }}</text>
<text class="sport-desc">{{ (sport.description ?? '暂无描述') as string }}</text>
<view class="sport-meta">
<text class="meta-item">难度: {{ getDifficultyText((sport.difficulty_level as number) ?? 1) }}</text>
<text class="meta-item">{{ (sport.getNumber("calorie_rate") as number) ?? 0 }}卡/分钟</text>
</view>
</view>
</view>
<view v-if="getUserPreference(sport.id as string)" class="preference-info">
<view class="preference-row">
<text class="preference-label">喜好程度:</text>
<view class="rating-stars">
<text v-for="star in 5" :key="star"
class="star"
:class="{ 'filled': star <= ((getUserPreference(sport.id as string)?.preference_level as number) ?? 0) }"
@click="updatePreferenceLevel(sport.id as string, star)">
</text>
</view>
</view>
<view class="preference-row">
<text class="preference-label">每周{{ (getUserPreference(sport.id as string)?.frequency_per_week ?? '').toString() }}次</text>
<text class="preference-label">每次{{ (getUserPreference(sport.id as string)?.duration_minutes ?? '').toString() }}分钟</text>
</view>
<view class="preference-actions">
<button @click="editPreference(sport)" class="edit-pref-btn">设置详情</button>
<button v-if="getUserPreference(sport.id as string)?.is_favorite === true"
@click="toggleFavorite(sport.id as string)"
class="favorite-btn active">
<simple-icon type="heart" :size="12" color="#FF4757" />
<text>最爱</text>
</button>
<button v-else @click="toggleFavorite(sport.id as string)" class="favorite-btn">
<simple-icon type="heart" :size="12" color="#999" />
<text>设为最爱</text>
</button>
</view>
</view>
</view>
</view>
</view>
<!-- Recommended Sports -->
<view v-if="recommendedSports.length > 0" class="recommendations-section">
<view class="section-title">推荐运动</view>
<text class="section-desc">根据你的偏好为你推荐</text>
<view class="recommended-list">
<view v-for="sport in recommendedSports" :key="sport.id"
class="recommended-item" @click="addRecommendedSport(sport)">
<view class="recommended-icon">
<text class="recommended-emoji">{{ getSportIcon(sport) }}</text>
</view>
<view class="recommended-info">
<text class="recommended-name">{{ sport.name }}</text>
<text class="recommended-reason">{{ sport.recommendation_reason }}</text>
</view>
<simple-icon type="plus-circle" :size="20" color="#4CAF50" />
</view>
</view>
</view>
</scroll-view>
<!-- Edit Preference Modal -->
<view v-if="showPreferenceModal" class="modal-overlay" @click="closePreferenceModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">设置运动偏好</text>
<button @click="closePreferenceModal" class="modal-close">
<simple-icon type="x" :size="20" color="#666" />
</button>
</view>
<view class="modal-body">
<view class="sport-preview">
<view class="preview-icon">
<text class="preview-emoji">{{ getSportIcon(editingSport) }}</text>
</view>
<view class="preview-info">
<text class="preview-name">{{ editingSport?.name }}</text>
<text class="preview-category">{{ editingSport?.category }}</text>
</view>
</view>
<view class="form-group">
<text class="form-label">喜好程度</text>
<view class="rating-selector">
<text v-for="level in 5" :key="level"
class="rating-star"
:class="{ 'selected': level <= (preferenceForm['preference_level'] as number) }"
@click="preferenceForm['preference_level'] = level as number">
</text>
</view>
<text class="rating-text">{{ getRatingText(preferenceForm['preference_level'] as number) }}</text>
</view>
<view class="form-group">
<text class="form-label">每周频次: {{ preferenceForm['frequency_per_week'] as number }}次</text>
<slider :value="preferenceForm['frequency_per_week'] as number"
min="1" max="7"
@change="onFrequencyChange"
activeColor="#4CAF50" />
</view>
<view class="form-group">
<text class="form-label">每次时长: {{ preferenceForm['duration_minutes'] as number }}分钟</text>
<slider :value="preferenceForm['duration_minutes'] as number"
min="10" max="120" step="5"
@change="onDurationChange"
activeColor="#4CAF50" />
</view>
<view class="form-group">
<text class="form-label">强度等级: {{ getIntensityText(preferenceForm['intensity_level'] as number) }}</text>
<slider :value="preferenceForm['intensity_level'] as number"
min="1" max="5"
@change="onIntensityChange"
activeColor="#4CAF50" />
</view>
<view class="form-group">
<text class="form-label">备注</text>
<textarea :value="preferenceForm['notes'] as string" class="form-textarea"
placeholder="记录你对这项运动的想法..." maxlength="200"></textarea>
</view>
</view>
<view class="modal-footer">
<button @click="removePreference" class="remove-btn">移除偏好</button>
<button @click="closePreferenceModal" class="cancel-btn">取消</button>
<button @click="savePreference" class="save-btn">保存</button>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts"> import { ref, onMounted, computed } from 'vue'
import { onResize, onLoad } from '@dcloudio/uni-app'
import { getCurrentUserId } from '@/utils/store'
import supaClient from '@/components/supadb/aksupainstance.uts'
// 响应式数据
const loading = ref(true)
const showSearch = ref(false)
const searchQuery = ref('')
const selectedCategory = ref('all')
const sports = ref<UTSJSONObject[]>([])
const userPreferences = ref<UTSJSONObject[]>([])
const recommendedSports = ref<UTSJSONObject[]>([])
const contentHeight = ref(0)
// 新增:全局 userId 响应式变量
const userId = ref<string>('')
// Responsive state - using onResize for dynamic updates
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
// Computed properties for responsive design
const isLargeScreen = computed(() : boolean => {
return screenWidth.value >= 768
})
// 模态框相关
const showPreferenceModal = ref(false)
const editingSport = ref<UTSJSONObject | null>(null)
const preferenceForm = ref({
preference_level: 3,
frequency_per_week: 2,
duration_minutes: 30,
intensity_level: 2,
notes: ''
})
// 分类数据
const categories = [
{ key: 'all', name: '全部', icon: '🏃' },
{ key: '有氧运动', name: '有氧运动', icon: '💨' },
{ key: '力量训练', name: '力量训练', icon: '💪' },
{ key: '球类运动', name: '球类运动', icon: '⚽' },
{ key: '柔韧训练', name: '柔韧训练', icon: '🤸' },
{ key: '水上运动', name: '水上运动', icon: '🏊' },
{ key: '户外运动', name: '户外运动', icon: '🚴' }
]
// 计算属性
const filteredSports = computed(() => {
let result = sports.value
// 按分类过滤
if (selectedCategory.value !== 'all') {
result = result.filter(sport => sport['category'] === selectedCategory.value)
}
// 按搜索词过滤
if (searchQuery.value !== '') {
const query = searchQuery.value.toString().toLowerCase()
result = result.filter(sport => {
const name = (sport['name'] ?? '').toString().toLowerCase()
const description = (sport['description'] ?? '').toString().toLowerCase()
return name.includes(query) || description.includes(query)
})
}
return result
})
const favoriteCount = computed(() => {
return userPreferences.value.filter(pref => pref['is_favorite'] === true).length.toString()
})
const totalWeeklyTime = computed(() => {
return userPreferences.value.reduce((total, pref) => {
const frequency = pref['frequency_per_week'] as number ?? 0
const duration = pref['duration_minutes'] as number ?? 0
return total + (frequency * duration)
}, 0 as number)
})
const averageIntensity = computed(() => {
if (userPreferences.value.length === 0) return 0
const totalIntensity = userPreferences.value.reduce((total, pref) => {
const raw = pref['intensity_level']
const intensity = raw != null ? (raw as number) : 1
return total + intensity
}, 0 as number)
return Math.round(totalIntensity / userPreferences.value.length * 10) / 10
})
// 计算内容高度
const calculateContentHeight = () => {
const systemInfo = uni.getSystemInfoSync()
const windowHeight = systemInfo.windowHeight
const headerHeight = 60
const searchHeight = showSearch.value ? 50 : 0
contentHeight.value = windowHeight - headerHeight - searchHeight
}
// 加载运动类型
const loadSportTypes = async () => {
const result = await supaClient
.from('ak_sport_types')
.select('*',{count:'exact'})
.eq('is_active', true)
.order('category', { ascending: true })
.execute()
if (result.data!=null) {
sports.value = result.data as UTSJSONObject[]
}
}
// 加载用户偏好
const loadUserPreferences = async () => {
if (userId.value == null || userId.value === '') return
const result = await supaClient
.from('ak_user_sport_preferences')
.select(`
*,
sport_type:ak_sport_types(*)
`,{count:'exact'})
.eq('user_id', userId.value)
.execute()
if (result.data != null) {
userPreferences.value = result.data as UTSJSONObject[]
}
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
// 搜索相关
const onSearchInput = () => {
// 实时搜索,这里可以添加防抖逻辑
}
const clearSearch = () => {
searchQuery.value = ''
}
// 分类选择
const selectCategory = (category: string) => {
selectedCategory.value = category
}
// 创建默认偏好
const createDefaultPreference = async (sport: UTSJSONObject) => {
try {
if (userId.value == null || userId.value === '') return
const preferenceData = {
user_id: userId.value,
sport_type_id: (sport['id'] ?? ''),
preference_level: 3,
frequency_per_week: 2,
duration_minutes: 30,
intensity_level: 2,
is_favorite: false
}
const result = await supaClient
.from('ak_user_sport_preferences')
.insert(preferenceData)
.execute()
const err = result.error
if (err != null) {
uni.showToast({
title: err.message,
icon: 'none'
})
return
}
uni.showToast({
title: '已添加到偏好',
icon: 'success'
})
loadUserPreferences()
} catch (error) {
console.error('创建偏好失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
// 工具函数
const getUserPreference = (sportId: string): UTSJSONObject | null => {
const found = userPreferences.value.find(pref => {
return pref['sport_type_id'] === sportId
})
return found != null ? found as UTSJSONObject : null
}
const getSportIcon = (sport: UTSJSONObject | null): string => {
if (sport == null) return '🏃'
let category = ''
if (sport != null) {
const cat = sport['category']
category = typeof cat === 'string' ? cat : ''
}
const icons = {
'有氧运动': '🏃',
'力量训练': '🏋️',
'球类运动': '⚽',
'柔韧训练': '🤸',
'水上运动': '🏊',
'户外运动': '🚴'
}
const icon = icons[category]
return typeof icon === 'string' ? icon : '🏃'
}
const getCategoryName = (key: string): string => {
const category = categories.find(c => c.key === key)
return category != null ? (category['name'] as string) : '全部'
}
const getDifficultyText = (level: number): string => {
const texts = ['', '入门', '初级', '中级', '高级', '专业']
const text = texts[level]
return typeof text === 'string' ? text : '未知'
}
const getRatingText = (level: number): string => {
const texts = ['', '不太喜欢', '一般', '喜欢', '很喜欢', '非常喜欢']
const text = texts[level]
return typeof text === 'string' ? text : '一般'
}
const getIntensityText = (level: number): string => {
const texts = ['', '很轻松', '轻松', '中等', '较高', '很高']
const text = texts[level]
return typeof text === 'string' ? text : '中等'
}
const getRecommendationReason = (rec: any): string => {
const reasons = [
'适合你的健身水平',
'与你的喜好相匹配',
'帮助达成训练目标',
'平衡你的运动类型',
'新手友好的选择'
]
return reasons[Math.floor(Math.random() * reasons.length)]
}
// 编辑偏好
const editPreference = (sport: UTSJSONObject) => {
editingSport.value = sport
const preference = getUserPreference(sport['id'] as string)
if (preference != null) {
preferenceForm.value = {
preference_level: preference['preference_level'] as number ?? 3,
frequency_per_week: preference['frequency_per_week'] as number ?? 2,
duration_minutes: preference['duration_minutes'] as number ?? 30,
intensity_level: preference['intensity_level'] as number ?? 2,
notes: preference['notes'] as string ?? ''
}
} else {
preferenceForm.value = {
preference_level: 3,
frequency_per_week: 2,
duration_minutes: 30,
intensity_level: 2,
notes: ''
}
}
showPreferenceModal.value = true
}
// 关闭偏好模态框
const closePreferenceModal = () => {
showPreferenceModal.value = false
editingSport.value = null
}
// 保存偏好
const savePreference = async () => {
try {
if (userId.value == null || userId.value === '' || editingSport.value == null) return
const editing = editingSport.value
const sportId = (editing?.['id'] ?? '') as string
const existing = getUserPreference(sportId)
const preferenceData = {
user_id: userId.value,
sport_type_id: sportId,
...preferenceForm.value
}
if (existing != null) {
const result = await supaClient
.from('ak_user_sport_preferences')
.update(preferenceData)
.eq('id', existing?.['id'] ?? '')
.execute()
const reserr = result.error
if (reserr != null) {
uni.showToast({
title: reserr.message,
icon: 'none'
})
return
}
} else {
const result = await supaClient
.from('ak_user_sport_preferences')
.insert(preferenceData)
.execute()
const reserr = result.error
if (reserr != null) {
uni.showToast({
title: reserr.message,
icon: 'none'
})
return
}
}
uni.showToast({
title: '保存成功',
icon: 'success'
})
closePreferenceModal()
loadUserPreferences()
} catch (error) {
console.error('保存偏好失败:', error)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
// 移除偏好
const removePreference = () => {
uni.showModal({
title: '移除偏好',
content: '确定要移除这个运动偏好吗?',
success: function(res) {
(async () => {
if (res.confirm) {
try {
const sportId = editingSport.value?.['id'] ?? ''
const existing = getUserPreference(sportId as string)
if (existing!= null) {
const result = await supaClient
.from('ak_user_sport_preferences')
.delete()
.eq('id', existing?.['id'] ?? '')
.execute()
const reserr = result.error
if (reserr != null) {
uni.showToast({
title: reserr.message,
icon: 'none'
})
return
}
uni.showToast({
title: '已移除',
icon: 'success'
})
closePreferenceModal()
loadUserPreferences()
}
} catch (error) {
console.error('移除偏好失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
})()
}
})
}
// 切换最爱
const toggleFavorite = async (sportId: string) => {
try {
const preference = getUserPreference(sportId)
if (preference == null) return
const newFavoriteStatus = ((preference?.['is_favorite'] ?? false) as boolean) ? false : true
const result = await supaClient
.from('ak_user_sport_preferences')
.update({ is_favorite: newFavoriteStatus })
.eq('id', preference?.['id'] ?? '')
.execute()
const reserr = result.error
if (reserr != null) {
uni.showToast({
title: reserr.message,
icon: 'none'
})
return
}
uni.showToast({
title: newFavoriteStatus ? '已设为最爱' : '已取消最爱',
icon: 'success'
})
loadUserPreferences()
} catch (error) {
console.error('更新最爱状态失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
// 更新偏好等级
const updatePreferenceLevel = async (sportId: string, level: number) => {
try {
const preference = getUserPreference(sportId)
if (preference == null) return
const result = await supaClient
.from('ak_user_sport_preferences')
.update({ preference_level: level })
.eq('id', preference?.['id'] ?? '')
.execute()
const reserr = result.error
if (reserr != null) {
uni.showToast({
title: reserr.message,
icon: 'none'
})
return
}
loadUserPreferences()
} catch (error) {
console.error('更新偏好等级失败:', error)
}
}
// 添加推荐运动
const addRecommendedSport = (sport: UTSJSONObject) => {
createDefaultPreference(sport)
}
// 表单事件处理
const onFrequencyChange = (e:UniSliderChangeEvent) => {
preferenceForm.value['frequency_per_week'] = e.detail.value
}
const onDurationChange = (e:UniSliderChangeEvent) => {
preferenceForm.value['duration_minutes'] = e.detail.value
}
const onIntensityChange = (e:UniSliderChangeEvent) => {
preferenceForm.value['intensity_level'] = e.detail.value
}
// 加载推荐
const loadRecommendations = async () => {
if (userId.value == null || userId.value === '') return
try {
const result = await supaClient.rpc('get_training_recommendations', {
p_user_id: userId.value
})
if (result.data!=null) {
const recommendations = Array.isArray(result.data) ? result.data : [result.data]
const recList: UTSJSONObject[] = []
for (let i = 0; i < recommendations.length; i++) {
const rec = recommendations[i] as UTSJSONObject
recList.push({
...rec,
recommendation_reason: getRecommendationReason(rec)
})
}
recommendedSports.value = recList
}
else
{
console.log('get_training_recommendations null')
}
} catch (error) {
console.log('推荐功能暂不可用')
}
}
// 加载数据
const loadData = async () => {
try {
loading.value = true
await Promise.all([
loadSportTypes(),
loadUserPreferences(),
loadRecommendations()
])
} catch (error) {
console.error('加载数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
calculateContentHeight()
loadData()
}) // Lifecycle hooks
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
})
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
onLoad((options: OnLoadOptions) => {
userId.value = options['id'] ?? getCurrentUserId();
})
// 工具函数下方新增
const toggleSportPreference = (sport: UTSJSONObject) => {
const pref = getUserPreference(sport['id'] as string)
if (pref == null) {
createDefaultPreference(sport)
} else {
editPreference(sport)
}
}
</script>
<style scoped>
.favorite-exercises-page {
display: flex;
flex:1;
background-color: #f5f5f5;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.header {
height: 60px; background-image: linear-gradient(to top right, #4CAF50, #45a049);
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.header-left {
flex-direction: row;
align-items: center;
flex: 1;
}
.back-btn, .search-btn {
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 20px;
padding: 8px 12px;
flex-direction: row;
align-items: center;
}
.back-btn simple-icon,
.search-btn simple-icon {
margin-right: 4px;
}
.back-btn {
margin-right: 12px;
}
.back-btn text, .search-btn text {
color: #FFFFFF;
font-size: 14px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #FFFFFF;
}
.search-bar {
background-color: #FFFFFF;
padding: 12px 16px;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.search-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 20px;
font-size: 14px;
}
.clear-btn {
background: transparent;
border: none;
padding: 8px;
margin-left: 8px;
}
.content {
flex: 1;
padding: 16px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.section-desc {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.summary-section {
margin-bottom: 24px;
}
.summary-stats {
flex-direction: row;
gap: 12px;
}
.stat-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
flex: 1;
align-items: center;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
.category-section {
margin-bottom: 24px;
}
.category-tabs {
flex:1;
flex-direction: row;
gap: 8px;
height: 80px;
}
.category-tab {
background-color: #FFFFFF;
border-radius: 12px;
padding: 12px 16px;
align-items: center;
text-align: center;
min-width: 80px;
border: 2px solid transparent;
}
.category-tab.active {
border-color: #4CAF50;
background-color: #E8F5E8;
}
.category-emoji {
font-size: 20px;
margin-bottom: 4px;
}
.category-name {
font-size: 12px;
color: #666;
}
.category-tab.active .category-name {
color: #4CAF50;
font-weight: bold;
}
.sports-section {
margin-bottom: 24px;
}
.empty-state {
background-color: #FFFFFF;
border-radius: 12px;
padding: 40px 20px;
align-items: center;
text-align: center;
}
.empty-text {
font-size: 16px;
color: #666;
margin: 12px 0 4px;
}
.empty-desc {
font-size: 14px;
color: #999;
}
.sports-list {
gap: 12px;
}
.sport-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.sport-header {
flex-direction: row;
align-items: center;
margin-bottom: 12px;
}
.sport-icon {
width: 40px;
height: 40px;
background-color: #f0f0f0;
border-radius: 20px;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.sport-emoji {
font-size: 20px;
}
.sport-info {
flex: 1;
}
.sport-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.sport-desc {
font-size: 12px;
color: #666;
margin-bottom: 8px;
}
.sport-meta {
flex-direction: row;
gap: 12px;
}
.meta-item {
font-size: 11px;
color: #999;
background-color: #f8f9fa;
padding: 2px 6px;
border-radius: 8px;
}
.preference-info {
border-top: 1px solid #f0f0f0;
padding-top: 12px;
gap: 8px;
}
.preference-row {
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.preference-label {
font-size: 14px;
color: #666;
}
.rating-stars {
flex-direction: row;
gap: 2px;
}
.star {
font-size: 16px;
color: #ddd;
}
.star.filled {
color: #FFD700;
}
.preference-actions {
flex-direction: row;
gap: 8px;
margin-top: 8px;
}
.edit-pref-btn, .favorite-btn {
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
border: none;
flex-direction: row;
align-items: center;
gap: 4px;
}
.edit-pref-btn {
background-color: #4CAF50;
color: #FFFFFF;
}
.favorite-btn {
background-color: #f5f5f5;
color: #666;
}
.favorite-btn.active {
background-color: #FFE5E5;
color: #FF4757;
}
.recommendations-section {
margin-bottom: 24px;
}
.recommended-list {
gap: 8px;
}
.recommended-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 12px;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
}
.recommended-icon {
width: 32px;
height: 32px;
background-color: #f0f0f0;
border-radius: 16px;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.recommended-emoji {
font-size: 16px;
}
.recommended-info {
flex: 1;
}
.recommended-name {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 2px;
}
.recommended-reason {
font-size: 12px;
color: #666;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: #FFFFFF;
border-radius: 12px;
width: 85%;
max-height: 80%;
}
.modal-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.modal-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.modal-close {
background: transparent;
border: none;
padding: 4px;
}
.modal-body {
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.sport-preview {
flex-direction: row;
align-items: center;
padding: 12px;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 16px;
}
.preview-icon {
width: 40px;
height: 40px;
background-color: #FFFFFF;
border-radius: 20px;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.preview-emoji {
font-size: 20px;
}
.preview-info {
flex: 1;
}
.preview-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 2px;
}
.preview-category {
font-size: 12px;
color: #666;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
display: block;
}
.form-textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
height: 80px;
text-align: start;
}
.rating-selector {
flex-direction: row;
gap: 4px;
margin-bottom: 8px;
}
.rating-star {
font-size: 24px;
color: #ddd;
padding: 4px;
}
.rating-star.selected {
color: #FFD700;
}
.rating-text {
font-size: 12px;
color: #666;
text-align: center;
}
.modal-footer {
flex-direction: row;
justify-content: space-between;
padding: 16px;
border-top: 1px solid #f0f0f0;
}
.remove-btn, .cancel-btn, .save-btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
border: none;
}
.remove-btn {
background-color: #FF4757;
color: #FFFFFF;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
}
.save-btn {
background-color: #4CAF50;
color: #FFFFFF;
}
.loading-container {
flex: 1;
justify-content: center;
align-items: center;
}
.loading-text {
font-size: 16px;
color: #666;
}
</style>