Files
sports-empower-hub/app/citizen-frontend/src/pages/index.vue
T
skytek_xinliang 889e44f68e feat: 課程建議
2026-08-05 15:45:45 +08:00

336 lines
9.2 KiB
Vue
Raw 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>
<Layout>
<template #main>
<template v-if="store.isLoggedIn">
<!-- 頂部我的健康力護照卡片 (Taipei Masters Hub) -->
<v-card class="border-primary border-xl rounded-xl" variant="flat">
<v-row class="mt-4" no-gutters>
<v-col cols="8">
<v-card-title> {{ store.userProfile.name }} 先生 </v-card-title>
<v-card-subtitle> 資料更新於 {{ todayTimeString }} </v-card-subtitle>
</v-col>
<v-col class="d-flex justify-center" cols="4">
<!-- 健康力分數大圓盤 -->
<div class="score-disk d-flex flex-column justify-center align-center">
<span class="score-val">{{ displayedHealthScore }}</span>
<span class="score-unit"></span>
</div>
</v-col>
</v-row>
<!-- 四項核心能力數值簡覽 -->
<v-row class="mt-4" no-gutters>
<v-col class="text-center border pa-2" cols="3">
<div class="">肌力</div>
<div class="font-weight-black">
{{ store.radarData.strength }}
</div>
</v-col>
<v-col class="text-center border pa-2" cols="3">
<div class="">平衡</div>
<div class="font-weight-black">
{{ store.radarData.balance }}
</div>
</v-col>
<v-col class="text-center border pa-2" cols="3">
<div class="">敏捷</div>
<div class="font-weight-black">
{{ store.radarData.agility }}
</div>
</v-col>
<v-col class="text-center border pa-2" cols="3">
<div class="">心肺</div>
<div class="font-weight-black">
{{ store.radarData.cardio }}
</div>
</v-col>
</v-row>
<!-- 課程建議入口 -->
<v-list-item append-icon="mdi-chevron-right" class="bg-primary py-4" to="/course">
<v-list-item-title class="font-weight-bold text-accent-light">
<v-icon icon="mdi-notebook-check-outline" size="16" />
課程建議
</v-list-item-title>
<v-list-item-subtitle class="text-white mt-2">
依你的體適能組別查看一週的每日課表
</v-list-item-subtitle>
</v-list-item>
</v-card>
<!-- 中間功能指標與五維雷達圖 -->
<v-card class="border-xl border-primary rounded-xl mt-4" variant="flat">
<v-card-title> 個人五維健康力指標 </v-card-title>
<v-card-subtitle>
綜合 11 項現場普測數值計算得出<br />藍色為同齡正常參考值區間
</v-card-subtitle>
<HealthRadarChart :radar-data="store.radarData" />
</v-card>
<ModuleRouterCard
description="了解公益普測計畫、研究依據與平台流程。"
icon="mdi-information-outline"
icon-color="primary"
title="簡介"
to="/introduction"
/>
<ModuleRouterCard
description="完成線上自評,取得初步風險分級。"
icon="mdi-clipboard-text-search"
icon-color="accent"
title="自評"
to="/self-assessment"
/>
<ModuleRouterCard
description="完成數值紀錄,建立個人功能基線。"
icon="mdi-run-fast"
icon-color="accent"
title="量測"
to="/checkin"
/>
<ModuleRouterCard
description="預訂首次量測或回測,追蹤健康狀態。"
icon="mdi-calendar-clock"
icon-color="accent"
title="預約"
to="/appointment"
/>
</template>
<template v-else>
<div class="text-center mb-6">
<BrandAvatar class="elevation-2" :icon-size="42" :size="72" />
</div>
<v-card class="rounded-xl elevation-2 mb-4 py-4" color="surface">
<v-card-text>
<div class="d-flex align-center mb-4">
<v-avatar class="mr-4 text-white" color="accent" size="36">
<v-icon icon="mdi-clipboard-text-outline" />
</v-avatar>
<div class="text-left">
<div class="text-title-medium font-weight-bold text-primary">
無須登入先做線上健康自評
</div>
<div class="text-title-small text-grey mt-2">
3 分鐘完成取得初步健康風險分級
</div>
</div>
</div>
<v-btn
block
class="rounded-lg font-weight-bold elevation-1 mb-3"
color="secondary"
prepend-icon="mdi-clipboard-text-search"
to="/self-assessment"
variant="flat"
>
開始自評
</v-btn>
<v-btn
block
class="rounded-lg font-weight-bold"
color="primary"
prepend-icon="mdi-login"
to="/login"
variant="outlined"
>
登入平台
</v-btn>
<div class="d-flex align-center mt-4">
<v-icon class="mr-3" color="primary" icon="mdi-lock-outline" size="28" />
<div>
<div class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div>
<div class="text-title-small text-grey">
普測預約現場報到量測紀錄與運動處方
</div>
</div>
</div>
</v-card-text>
</v-card>
<ModuleRouterCard
description="了解公益普測計畫、研究依據與平台流程。"
icon="mdi-information-outline"
icon-color="primary"
title="簡介"
to="/introduction"
/>
</template>
</template>
</Layout>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import BrandAvatar from '@/components/BrandAvatar.vue'
import HealthRadarChart from '@/components/HealthRadarChart.vue'
import Layout from '@/components/layout/DefaultLayout.vue'
import ModuleRouterCard from '@/components/ModuleRouterCard.vue'
import { useAppStore } from '@/stores/app'
const store = useAppStore()
const displayedHealthScore = ref(1)
let healthScoreTimer: number | undefined
function animateHealthScore (targetScore: number) {
if (healthScoreTimer !== undefined) {
window.clearInterval(healthScoreTimer)
}
if (targetScore <= 1) {
displayedHealthScore.value = targetScore
return
}
displayedHealthScore.value = 1
const duration = 900
const stepDuration = Math.max(12, Math.floor(duration / targetScore))
healthScoreTimer = window.setInterval(() => {
if (displayedHealthScore.value >= targetScore) {
window.clearInterval(healthScoreTimer)
healthScoreTimer = undefined
return
}
displayedHealthScore.value += 1
}, stepDuration)
}
onMounted(() => {
animateHealthScore(store.healthScore)
})
onBeforeUnmount(() => {
if (healthScoreTimer !== undefined) {
window.clearInterval(healthScoreTimer)
}
})
watch(
() => store.healthScore,
score => {
animateHealthScore(score)
},
)
const todayTimeString = computed(() => {
const now = new Date()
const date = [
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
].join('/')
const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
return `${date} ${time}`
})
</script>
<style scoped>
.passport-card {
background: linear-gradient(135deg, #1d7063 0%, #145248 100%);
border: 1px solid rgba(71, 194, 166, 0.3);
box-shadow: 0 8px 16px rgba(20, 82, 72, 0.25) !important;
}
.bg-circle-1 {
position: absolute;
width: 150px;
height: 150px;
border-radius: 50%;
background: rgba(71, 194, 166, 0.15);
top: -50px;
right: -50px;
z-index: 0;
}
.bg-circle-2 {
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
background: rgba(230, 105, 38, 0.1);
bottom: -30px;
left: -20px;
z-index: 0;
}
.z-index-1 {
z-index: 1;
}
.bg-white-20 {
background-color: rgba(255, 255, 255, 0.2);
}
.bg-white-10 {
background-color: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.text-white-70 {
color: rgba(255, 255, 255, 0.7) !important;
}
.text-accent-light {
color: #8cebd2 !important;
}
.score-disk {
background: radial-gradient(circle, #e66926 0%, #cc561b 100%);
border: 3px solid #ffffff;
width: 72px;
height: 72px;
border-radius: 50%;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
flex-shrink: 0;
}
.score-val {
font-size: 1.75rem;
font-weight: 900;
line-height: 1;
color: #ffffff;
}
.score-unit {
font-size: 0.65rem;
font-weight: bold;
color: rgba(255, 255, 255, 0.8);
}
.transition-card {
transition: all 0.25s ease;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.transition-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.06);
}
</style>