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

1242 lines
30 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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>
<view class="preferences-analytics-page">
<!-- 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="refreshData" class="refresh-btn">
<simple-icon type="refresh" :size="16" color="#FFFFFF" />
<text>刷新</text>
</button>
</view>
</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' }">
<!-- Summary Statistics -->
<view class="summary-section">
<view class="section-title">偏好概览</view>
<view class="summary-cards">
<view class="summary-card">
<view class="card-icon">❤️</view>
<view class="card-content">
<text class="card-number">{{ analyticsData.getString("favoriteCount") }}</text>
<text class="card-label">喜爱运动</text>
</view>
</view>
<view class="summary-card">
<view class="card-icon">📅</view>
<view class="card-content">
<text class="card-number">{{ analyticsData.getString("weeklyHours") }}</text>
<text class="card-label">周计划时长(h)</text>
</view>
</view>
<view class="summary-card">
<view class="card-icon">🎯</view>
<view class="card-content">
<text class="card-number">{{ analyticsData.getString("activeGoals") }}</text>
<text class="card-label">活跃目标</text>
</view>
</view>
<view class="summary-card">
<view class="card-icon">⏰</view>
<view class="card-content">
<text class="card-number">{{ analyticsData.getString("remindersCount") }}</text>
<text class="card-label">活跃提醒</text>
</view>
</view>
</view>
</view>
<!-- Exercise Preferences Distribution -->
<view class="distribution-section">
<view class="section-title">运动类型分布</view>
<view class="chart-container">
<view class="chart-legend">
<view v-for="category in categoryDistribution" :key="category.name" class="legend-item">
<view class="legend-color" :style="{ backgroundColor: category.color }"></view>
<text class="legend-text">{{ category.name }}</text>
<text class="legend-value">{{ category.count }}</text>
</view>
</view>
<view class="distribution-bars">
<view v-for="category in categoryDistribution" :key="category.name" class="distribution-bar">
<view class="bar-info">
<text class="bar-label">{{ category.name }}</text>
<text class="bar-percentage">{{ category.percentage }}%</text>
</view>
<view class="bar-track">
<view class="bar-fill" :style="{
width: category.getString('percentage') + '%',
backgroundColor: category.getString('color')
}"></view>
</view>
</view>
</view>
</view>
</view>
<!-- Intensity Analysis -->
<view class="intensity-section">
<view class="section-title">训练强度分析</view>
<view class="intensity-chart">
<view class="intensity-levels">
<view v-for="level in intensityAnalysis" :key="level.level" class="intensity-level">
<view class="level-header">
<text class="level-name">{{ level.name }}</text>
<text class="level-count">{{ level.count }}项</text>
</view>
<view class="level-bar">
<view class="level-fill" :style="{
width: level.getString('percentage') + '%',
backgroundColor: level.getString('color')
}"></view>
</view>
</view>
</view>
</view>
</view>
<!-- Weekly Schedule Analysis -->
<view class="schedule-section">
<view class="section-title">训练时间安排</view>
<view class="weekly-schedule">
<view class="time-periods">
<view v-for="period in timePeriods" :key="period.getString('name')" class="time-period">
<text class="period-name">{{ period.getString('name') }}</text>
<view class="period-days">
<view v-for="day in (period.days as Array<UTSJSONObject>)" :key="day.getString('name')"
class="day-item" :class="{ 'has-training': day.getBoolean('hasTraining') }">
<text class="day-name">{{ day.getString('name') }}</text>
<text v-if="day.getBoolean('hasTraining')"
class="day-count">{{ day.getNumber('sessionCount') }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- Goals Progress -->
<view class="goals-section">
<view class="section-title">目标完成情况</view>
<view class="goals-list">
<view v-for="goal in goalsProgress" :key="goal.id" class="goal-item">
<view class="goal-header">
<text class="goal-type">{{ goal.type }}</text>
<text class="goal-progress-text">{{ goal.progressPercentage }}%</text>
</view>
<view class="goal-progress-bar">
<view class="goal-progress-fill" :style="{
width: goal.getString('progressPercentage') + '%',
backgroundColor: getProgressColor(goal.getNumber('progressPercentage')??0)
}"></view>
</view>
<view class="goal-meta">
<text class="goal-current">{{ goal.currentValue }}/{{ goal.targetValue }}
{{ goal.unit }}</text>
<text class="goal-status"
:class="getStatusClass(goal.getString('status')??'')">{{ goal.getString('status') }}</text>
</view>
</view>
</view>
</view>
<!-- Recommendations -->
<view class="recommendations-section">
<view class="section-title">智能建议</view>
<view class="recommendations-list">
<view v-for="recommendation in recommendations" :key="recommendation.id"
class="recommendation-item">
<view class="recommendation-icon">
<text class="rec-emoji">{{ recommendation.icon }}</text>
</view>
<view class="recommendation-content">
<text class="rec-title">{{ recommendation.title }}</text>
<text class="rec-description">{{ recommendation.description }}</text>
<view class="rec-actions">
<button @click="applyRecommendation(recommendation)" class="apply-btn">
<text>应用建议</text>
</button>
<button @click="dismissRecommendation(recommendation.getString('id')??'')"
class="dismiss-btn">
<text>忽略</text>
</button>
</view>
</view>
</view>
</view>
</view>
<!-- Trends Analysis -->
<view class="trends-section">
<view class="section-title">趋势分析</view>
<view class="trends-chart">
<view class="trend-metrics">
<view v-for="metric in trendMetrics" :key="metric.name" class="trend-metric">
<view class="metric-header">
<text class="metric-name">{{ metric.name }}</text>
<text class="metric-change" :class="metric.changeClass">{{ metric.change }}</text>
</view>
<view class="metric-chart">
<view v-for="(point, index) in metric.getArray('dataPoints')" :key="index"
class="data-point" :style="{ height: (point as Number) + '%' }">
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { onLoad, onReady, OnLoadOptions, onResize } from '@dcloudio/uni-app'
import { ref, onMounted, computed } from 'vue'
import { getCurrentUserId } from '@/utils/store'
import supaClient from '@/components/supadb/aksupainstance.uts'
const userId = ref('')
// 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 loading = ref(true)
const contentHeight = ref(0)
// 分析数据
const analyticsData = ref({
favoriteCount: 0,
weeklyHours: 0,
activeGoals: 0,
remindersCount: 0
})
const categoryDistribution = ref<UTSJSONObject[]>([])
const intensityAnalysis = ref<UTSJSONObject[]>([])
const timePeriods = ref<UTSJSONObject[]>([])
const goalsProgress = ref<UTSJSONObject[]>([])
const recommendations = ref<UTSJSONObject[]>([])
const trendMetrics = ref<UTSJSONObject[]>([])
// 加载概览数据
const loadSummaryData = async () => {
if (userId.value == null || userId.value == '') return
try {
// 获取喜爱运动数量
const favoritesResult = await supaClient
.from('ak_user_sport_preferences')
.select('id', { count: 'exact' })
.eq('user_id', userId.value)
.eq('is_favorite', true)
.execute()
// 获取总的训练时间
const preferencesResult = await supaClient
.from('ak_user_sport_preferences')
.select('frequency_per_week, duration_minutes', {})
.eq('user_id', userId.value)
.execute()
// 获取活跃目标
const goalsResult = await supaClient
.from('ak_user_training_goals')
.select('id', {})
.eq('user_id', userId.value)
.eq('status', 'active')
.execute()
// 获取活跃提醒
const remindersResult = await supaClient
.from('ak_training_reminders')
.select('id', {})
.eq('user_id', userId.value)
.eq('is_enabled', true)
.execute()
let weeklyMinutes = 0
if (preferencesResult.data != null && Array.isArray(preferencesResult.data)) {
(preferencesResult.data as Array<UTSJSONObject>).forEach((pref : UTSJSONObject) => {
const frequency = pref.getNumber('frequency_per_week') ?? 0
const duration = pref.getNumber('duration_minutes') ?? 0
weeklyMinutes += frequency * duration
})
}
analyticsData.value = {
favoriteCount: Array.isArray(favoritesResult.data) ? favoritesResult.data.length : 0,
weeklyHours: Math.round(weeklyMinutes / 60 * 10) / 10,
activeGoals: Array.isArray(goalsResult.data) ? goalsResult.data.length : 0,
remindersCount: Array.isArray(remindersResult.data) ? remindersResult.data.length : 0
}
} catch (error) {
console.error('加载概览数据失败:', error)
}
}
// 加载分类分布
const loadCategoryDistribution = async () => {
const userId = getCurrentUserId()
if (userId == null || userId == '') return
try {
const result = await supaClient
.from('ak_user_sport_preferences')
.select(`sport_type:ak_sport_types(category)`, {})
.eq('user_id', userId)
.execute()
if (Array.isArray(result.data)) {
const colors = ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#F44336', '#00BCD4']
let colorIndex = 0
const categoryArr : UTSJSONObject[] = []
// 用UTSJSONObject代替plain object来统计category
const categoryMap = {} as UTSJSONObject
// 用标准for循环替换forEach
const dataArr = result.data as Array<UTSJSONObject>
for (let i = 0; i < dataArr.length; i++) {
const pref = dataArr[i]
let category = '其他'
const sportType = pref['sport_type'] as UTSJSONObject
if (sportType != null && typeof sportType.getString === 'function') {
const cat = sportType.getString('category')
if (cat != null && cat.length > 0) category = cat
}
if (!categoryMap.hasOwnProperty(category)) {
categoryMap[category] = 1
} else {
categoryMap[category] = (categoryMap[category] as number) + 1
}
}
// UTS Supabase返回的result没有total字段直接用dataArr.length
const totalCount = dataArr.length
for (let category in categoryMap) {
if (!categoryMap.hasOwnProperty(category)) continue
const count = categoryMap[category] as number
categoryArr.push({
name: category,
count: count,
percentage: totalCount > 0 ? Math.round((count * 100.0) / totalCount) : 0,
color: colors[colorIndex % colors.length]
})
colorIndex++
}
categoryDistribution.value = categoryArr.sort((a, b) => (b['count'] as number) - (a['count'] as number))
}
} catch (error) {
console.error('加载分类分布失败:', error)
}
}
// intensityAnalysis部分修正
const loadIntensityAnalysis = async () => {
const userId = getCurrentUserId()
if (userId == null || userId == '') return
try {
const result = await supaClient
.from('ak_user_sport_preferences')
.select('intensity_level', {})
.eq('user_id', userId)
.execute()
if (Array.isArray(result.data)) {
const intensityCount = {} as UTSJSONObject
const dataArr = result.data as Array<UTSJSONObject>
for (let i = 0; i < dataArr.length; i++) {
const pref = dataArr[i]
let level = 1
if (typeof pref['intensity_level'] === 'number') {
level = pref['intensity_level'] as number
}
const levelKey = level + ''
if (!intensityCount.hasOwnProperty(levelKey)) {
intensityCount[levelKey] = 1
} else {
intensityCount[levelKey] = (intensityCount[levelKey] as number) + 1
}
}
const total = dataArr.length
const intensityNames = ['', '很轻松', '轻松', '中等', '较高', '很高']
const colors = ['', '#4CAF50', '#8BC34A', '#FFC107', '#FF9800', '#F44336']
const analysis : UTSJSONObject[] = []
for (let level = 1; level <= 5; level++) {
const levelKey = level + ''
const count = typeof intensityCount[levelKey] === 'number' ? (intensityCount[levelKey] as number) : 0
analysis.push({
level: level,
name: intensityNames[level],
count: count,
percentage: total > 0 ? Math.round((count * 100.0) / total) : 0,
color: colors[level]
})
}
intensityAnalysis.value = analysis
}
} catch (error) {
console.error('加载强度分析失败:', error)
}
}
// 加载日程分析部分修正
const loadScheduleAnalysis = async () => {
const userId = getCurrentUserId()
if (userId == null || userId == '') return
try {
const result = await supaClient
.from('ak_training_reminders')
.select('trigger_time, trigger_days', {})
.eq('user_id', userId)
.eq('is_enabled', true)
.execute()
const schedule = {} as UTSJSONObject
const timeSlots = ['早上', '上午', '下午', '晚上']
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
// 初始化时间表
for (let i = 0; i < timeSlots.length; i++) {
const slot = timeSlots[i]
const dayMap = {} as UTSJSONObject
for (let j = 0; j < weekDays.length; j++) {
dayMap[weekDays[j]] = 0
}
schedule[slot] = dayMap
}
if (Array.isArray(result.data)) {
for (let i = 0; i < result.data.length; i++) {
const reminder = result.data[i]
const triggerTime = (reminder as UTSJSONObject).getString('trigger_time') ?? ''
const triggerDays = (reminder as UTSJSONObject).getString('trigger_days') ?? ''
// UTS: 字符串判空需用 triggerTime != null && triggerTime.length > 0
if (triggerTime != null && triggerTime.length > 0 && triggerDays != null && triggerDays.length > 0) {
const hour = parseInt(triggerTime.split(':')[0])
let timeSlot = '早上'
if (hour >= 6 && hour < 9) timeSlot = '早上'
else if (hour >= 9 && hour < 12) timeSlot = '上午'
else if (hour >= 12 && hour < 18) timeSlot = '下午'
else timeSlot = '晚上'
for (let k = 0; k < triggerDays.length; k++) {
// UTS: triggerDays[k] 是字符,需转字符串
const dayIndex = parseInt(triggerDays.charAt(k) + '') - 1
if (dayIndex >= 0 && dayIndex < 7) {
const dayName = weekDays[dayIndex]
const slotMap = schedule[timeSlot] as UTSJSONObject
slotMap[dayName] = (slotMap[dayName] as number) + 1
}
}
}
}
}
const periods : UTSJSONObject[] = []
for (let i = 0; i < timeSlots.length; i++) {
const slot = timeSlots[i]
const slotMap = schedule[slot] as UTSJSONObject
if (slotMap != null) {
const days : UTSJSONObject[] = []
for (let j = 0; j < weekDays.length; j++) {
const day = weekDays[j]
let count = slotMap.getNumber(day) ?? 0
days.push({
name: day,
hasTraining: count > 0,
sessionCount: count
})
}
periods.push({
name: slot,
days: days
})
}
}
timePeriods.value = periods
} catch (error) {
console.error('加载日程分析失败:', error)
}
}
// 加载趋势指标
const loadTrendMetrics = async () => {
// 模拟趋势数据,实际应用中从数据库获取历史数据
trendMetrics.value = [
{
name: '训练频率',
change: '+12%',
changeClass: 'positive',
dataPoints: [60, 65, 70, 68, 75, 80, 85]
},
{
name: '目标完成',
change: '+8%',
changeClass: 'positive',
dataPoints: [40, 45, 50, 55, 60, 65, 70]
},
{
name: '运动多样性',
change: '+5%',
changeClass: 'positive',
dataPoints: [30, 35, 40, 45, 50, 55, 60]
}
]
}
// 工具函数
const getGoalTypeText = (atype : string) : string => {
const types = {
'weight_loss': '减肥目标',
'muscle_gain': '增肌目标',
'endurance': '耐力提升',
'flexibility': '柔韧性',
'strength': '力量增强',
'skill': '技能提升'
}
return types.getString(atype) ?? atype
}
const getGoalStatusText = (progress : number, status : string) : string => {
if (status === 'completed') return '已完成'
if (progress >= 100) return '已达成'
if (progress >= 75) return '接近完成'
if (progress >= 50) return '进行中'
if (progress >= 25) return '刚开始'
return '待开始'
}
const getProgressColor = (percentage : number) : string => {
if (percentage >= 100) return '#4CAF50'
if (percentage >= 75) return '#8BC34A'
if (percentage >= 50) return '#FFC107'
if (percentage >= 25) return '#FF9800'
return '#F44336'
}
const getStatusClass = (status : string) : string => {
if (status === '已完成' || status === '已达成') return 'status-completed'
if (status === '接近完成' || status === '进行中') return 'status-active'
return 'status-pending'
}
// 生成智能建议
const generateRecommendations = () => {
const recs : UTSJSONObject[] = []
const favoriteCount = analyticsData.value.getNumber("favoriteCount") ?? 0
// 基于数据生成建议
if (favoriteCount < 3) {
recs.push({
id: 'more_favorites',
icon: '❤️',
title: '扩展运动偏好',
description: '尝试添加更多喜爱的运动类型,让训练更有趣',
type: 'preference',
action: 'navigate_to_exercises'
})
}
const weeklyHours = analyticsData.value.getNumber("weeklyHours") ?? 0
if (weeklyHours < 3) {
recs.push({
id: 'increase_time',
icon: '⏰',
title: '增加训练时间',
description: '建议每周至少进行3小时的体育锻炼',
type: 'time',
action: 'adjust_schedule'
})
}
const activeGoals = analyticsData.value.getNumber("activeGoals") ?? 0
if (activeGoals === 0) {
recs.push({
id: 'set_goals',
icon: '🎯',
title: '设定训练目标',
description: '设定明确的训练目标有助于保持动力',
type: 'goal',
action: 'navigate_to_goals'
})
}
// 基于强度分析的建议
const highIntensityCount = intensityAnalysis.value.filter(item =>
((item as UTSJSONObject).getNumber('level') ?? 0) >= 4 && ((item as UTSJSONObject).getNumber('count') ?? 0) > 0
).length
if (highIntensityCount === 0) {
recs.push({
id: 'add_intensity',
icon: '💪',
title: '增加训练强度',
description: '适当增加高强度训练可以提高运动效果',
type: 'intensity',
action: 'adjust_intensity'
})
}
recommendations.value = recs
}
// 加载目标进度
const loadGoalsProgress = async () => {
const userId = getCurrentUserId()
if (userId == null || userId == '') return
try {
const result = await supaClient
.from('ak_user_training_goals')
.select('*', {})
.eq('user_id', userId)
.neq('status', 'cancelled')
.execute()
if (result.data != null && Array.isArray(result.data)) {
const goals : UTSJSONObject[] = []
const dataArr = result.data as Array<UTSJSONObject>
for (let i = 0; i < dataArr.length; i++) {
const goal = dataArr[i]
const targetValue = goal.getNumber('target_value') ?? 0
const currentValue = goal.getNumber('current_value') ?? 0
const progressPercentage = targetValue > 0 ? Math.min(100, Math.round((currentValue / targetValue) * 100)) : 0
const goalType = goal.getString('goal_type') ?? ''
const unit = goal.getString('unit') ?? ''
const status = goal.getString('status') ?? ''
const id = goal.getString('id') ?? ''
// 直接用 new Object() 明确声明为普通对象,避免 UTS Map 构造器冲突
const obj : UTSJSONObject = {}
obj.id = id
obj.type = getGoalTypeText(goalType)
obj.targetValue = targetValue
obj.currentValue = currentValue
obj.unit = unit
obj.progressPercentage = progressPercentage
obj.status = getGoalStatusText(progressPercentage, status)
goals.push(obj)
}
goalsProgress.value = goals.sort(function (a, b) {
return (b.progressPercentage as number) - (a.progressPercentage as number)
})
}
} catch (error) {
console.error('加载目标进度失败:', error)
}
}
// 加载分析数据
const loadAnalyticsData = async () => {
try {
loading.value = true
await Promise.all([
loadSummaryData(),
loadCategoryDistribution(),
loadIntensityAnalysis(),
loadScheduleAnalysis(),
loadGoalsProgress(),
loadTrendMetrics()
])
generateRecommendations()
} catch (error) {
console.error('加载分析数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 事件处理
const goBack = () => {
uni.navigateBack()
}
const refreshData = () => {
loadAnalyticsData()
}
const applyRecommendation = (recommendation : UTSJSONObject) => {
const action = (recommendation as UTSJSONObject).getString('action') ?? ''
switch (action) {
case 'navigate_to_exercises':
uni.navigateTo({
url: '/pages/sport/student/favorite-exercises'
})
break
case 'navigate_to_goals':
uni.navigateTo({
url: '/pages/sport/student/goal-settings'
})
break
case 'adjust_schedule':
uni.navigateTo({
url: '/pages/sport/student/reminder-settings'
})
break
default:
uni.showToast({
title: '建议已应用',
icon: 'success'
})
}
}
const dismissRecommendation = (id : string) => {
recommendations.value = recommendations.value.filter(rec =>
(rec as UTSJSONObject).getString('id') !== id
)
uni.showToast({
title: '建议已忽略',
icon: 'success'
})
}
// 生命周期
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
contentHeight.value = systemInfo.windowHeight - 120
})
// Lifecycle hooks
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
})
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
onLoad((opt : OnLoadOptions) => {
const idParam = opt['id'] ?? ''
if (idParam !== '') {
userId.value = idParam
} else {
userId.value = getCurrentUserId()
}
loadAnalyticsData()
})
</script>
<style scoped>
.preferences-analytics-page {
flex: 1;
background-color: #f5f5f5;
}
.header {
height: 60px;
background-image: linear-gradient(to top right, #667eea, #764ba2);
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,
.refresh-btn {
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 20px;
padding: 8px 12px;
flex-direction: row;
align-items: center;
gap: 4px;
}
.back-btn {
margin-right: 12px;
}
.back-btn text,
.refresh-btn text {
color: #FFFFFF;
font-size: 14px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #FFFFFF;
}
.header-actions {
flex-direction: row;
gap: 8px;
}
.loading-container {
flex: 1;
justify-content: center;
align-items: center;
}
.loading-text {
font-size: 16px;
color: #666;
}
.content {
flex: 1;
padding: 16px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
/* Summary Section */
.summary-section {
margin-bottom: 24px;
}
.summary-cards {
flex-direction: row;
gap: 12px;
}
.summary-card {
flex: 1;
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-icon {
font-size: 24px;
margin-bottom: 8px;
}
.card-content {
align-items: center;
}
.card-number {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.card-label {
font-size: 12px;
color: #666;
text-align: center;
}
/* Distribution Section */
.distribution-section {
margin-bottom: 24px;
}
.chart-container {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
}
.chart-legend {
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.legend-item {
flex-direction: row;
align-items: center;
gap: 8px;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 6px;
}
.legend-text {
font-size: 14px;
color: #333;
}
.legend-value {
font-size: 12px;
color: #666;
font-weight: bold;
}
.distribution-bars {
gap: 12px;
}
.distribution-bar {
margin-bottom: 12px;
}
.bar-info {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.bar-label {
font-size: 14px;
color: #333;
}
.bar-percentage {
font-size: 12px;
color: #666;
font-weight: bold;
}
.bar-track {
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 4px;
}
/* Intensity Section */
.intensity-section {
margin-bottom: 24px;
}
.intensity-chart {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
}
.intensity-levels {
gap: 12px;
}
.intensity-level {
margin-bottom: 12px;
}
.level-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.level-name {
font-size: 14px;
color: #333;
}
.level-count {
font-size: 12px;
color: #666;
}
.level-bar {
height: 6px;
background-color: #f0f0f0;
border-radius: 3px;
overflow: hidden;
}
.level-fill {
height: 100%;
border-radius: 3px;
}
/* Schedule Section */
.schedule-section {
margin-bottom: 24px;
}
.weekly-schedule {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
}
.time-periods {
gap: 16px;
}
.time-period {
margin-bottom: 16px;
}
.period-name {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.period-days {
flex-direction: row;
gap: 8px;
}
.day-item {
flex: 1;
background-color: #f8f9fa;
border-radius: 8px;
padding: 8px 4px;
align-items: center;
min-height: 48px;
justify-content: center;
}
.day-item.has-training {
background-color: #E8F5E8;
border: 1px solid #4CAF50;
}
.day-name {
font-size: 12px;
color: #666;
margin-bottom: 2px;
}
.day-count {
font-size: 10px;
color: #4CAF50;
font-weight: bold;
}
/* Goals Section */
.goals-section {
margin-bottom: 24px;
}
.goals-list {
gap: 12px;
}
.goal-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.goal-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.goal-type {
font-size: 16px;
font-weight: bold;
color: #333;
}
.goal-progress-text {
font-size: 14px;
color: #4CAF50;
font-weight: bold;
}
.goal-progress-bar {
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.goal-progress-fill {
height: 100%;
border-radius: 4px;
}
.goal-meta {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.goal-current {
font-size: 12px;
color: #666;
}
.goal-status {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
font-weight: bold;
}
.status-completed {
background-color: #E8F5E8;
color: #4CAF50;
}
.status-active {
background-color: #FFF3E0;
color: #FF9800;
}
.status-pending {
background-color: #FFEBEE;
color: #F44336;
}
/* Recommendations Section */
.recommendations-section {
margin-bottom: 24px;
}
.recommendations-list {
gap: 12px;
}
.recommendation-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
flex-direction: row;
gap: 12px;
margin-bottom: 12px;
}
.recommendation-icon {
width: 40px;
height: 40px;
background-color: #f0f0f0;
border-radius: 20px;
justify-content: center;
align-items: center;
}
.rec-emoji {
font-size: 20px;
}
.recommendation-content {
flex: 1;
}
.rec-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.rec-description {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.rec-actions {
flex-direction: row;
gap: 8px;
}
.apply-btn,
.dismiss-btn {
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
border: none;
}
.apply-btn {
background-color: #4CAF50;
color: #FFFFFF;
}
.dismiss-btn {
background-color: #f5f5f5;
color: #666;
}
/* Trends Section */
.trends-section {
margin-bottom: 24px;
}
.trends-chart {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
}
.trend-metrics {
gap: 16px;
}
.trend-metric {
margin-bottom: 16px;
}
.metric-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.metric-name {
font-size: 14px;
color: #333;
font-weight: bold;
}
.metric-change {
font-size: 12px;
font-weight: bold;
}
.metric-change.positive {
color: #4CAF50;
}
.metric-change.negative {
color: #F44336;
}
.metric-chart {
flex-direction: row;
align-items: end;
gap: 4px;
height: 40px;
}
.data-point {
flex: 1;
background-color: #4CAF50;
border-radius: 2px;
min-height: 4px;
}
</style>