456 lines
13 KiB
Vue
456 lines
13 KiB
Vue
<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>
|
||
{{ displayUserName }}
|
||
</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',
|
||
courseStore.groupLoaded
|
||
? `text-${courseStore.groupMeta.color}`
|
||
: 'text-primary',
|
||
]"
|
||
>
|
||
<v-progress-circular
|
||
v-if="courseStore.groupLoading"
|
||
color="primary"
|
||
indeterminate
|
||
size="28"
|
||
width="3"
|
||
/>
|
||
<span v-else-if="courseStore.groupLoaded" class="group-label">
|
||
{{ courseStore.groupMeta.label }}
|
||
</span>
|
||
<span v-else-if="courseStore.groupMissing" class="group-label">
|
||
未取得體適能分組
|
||
</span>
|
||
<span v-else class="group-label">無資料</span>
|
||
</div>
|
||
</v-col>
|
||
</v-row>
|
||
|
||
<v-card-subtitle v-if="courseStore.groupError" class="text-error">
|
||
{{ courseStore.groupError }}
|
||
<v-btn
|
||
class="ml-2"
|
||
color="error"
|
||
size="small"
|
||
variant="text"
|
||
@click="loadFitnessGroup"
|
||
>
|
||
重試
|
||
</v-btn>
|
||
</v-card-subtitle>
|
||
|
||
<!-- 四項核心能力數值簡覽 -->
|
||
<v-row v-if="RADAR_ENABLED" 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 mt-4"
|
||
to="/course"
|
||
>
|
||
<v-list-item-title class="font-weight-bold">
|
||
<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
|
||
v-if="RADAR_ENABLED"
|
||
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>
|
||
|
||
<v-dialog v-model="missingGroupDialog" max-width="400">
|
||
<v-card class="rounded-xl overflow-hidden text-center">
|
||
<v-card-title class="bg-primary text-white font-weight-bold py-3">
|
||
尚未取得體適能分組
|
||
</v-card-title>
|
||
|
||
<v-card-text class="pa-6">
|
||
<v-avatar class="mb-4" color="primary" size="64">
|
||
<v-icon color="white" icon="mdi-calendar-check" size="34" />
|
||
</v-avatar>
|
||
|
||
<p class="text-body-1 font-weight-bold text-grey-darken-4 mb-2">
|
||
預約普測,建立您的體適能分組
|
||
</p>
|
||
<p class="text-body-2 text-grey-darken-2 mb-0">
|
||
完成預約並到現場參加量測後,系統才能建立您的體適能分組與課程建議。
|
||
</p>
|
||
</v-card-text>
|
||
|
||
<v-divider />
|
||
|
||
<v-card-actions class="pa-4 justify-end">
|
||
<v-btn
|
||
class="rounded-lg"
|
||
color="primary"
|
||
variant="text"
|
||
@click="missingGroupDialog = false"
|
||
>
|
||
稍後再說
|
||
</v-btn>
|
||
<v-btn
|
||
class="rounded-lg font-weight-bold"
|
||
color="primary"
|
||
variant="flat"
|
||
@click="goToAppointment"
|
||
>
|
||
前往預約
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
</template>
|
||
</Layout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref, watch } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
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 { useDisplayUserName } from "@/composables/useDisplayUserName";
|
||
import { RADAR_ENABLED } from "@/config/features";
|
||
import { useAppStore } from "@/stores/app";
|
||
import { useCourseStore } from "@/stores/course";
|
||
|
||
const store = useAppStore();
|
||
const courseStore = useCourseStore();
|
||
const router = useRouter();
|
||
const missingGroupDialog = ref(false);
|
||
|
||
const displayUserName = useDisplayUserName(store.userProfile);
|
||
|
||
const MISSING_GROUP_DIALOG_KEY = "missing-fitness-group-dialog-shown";
|
||
|
||
function hasShownMissingGroupDialog() {
|
||
try {
|
||
return sessionStorage.getItem(MISSING_GROUP_DIALOG_KEY) === "true";
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function markMissingGroupDialogShown() {
|
||
try {
|
||
sessionStorage.setItem(MISSING_GROUP_DIALOG_KEY, "true");
|
||
} catch {
|
||
// 部分瀏覽器可能封鎖 sessionStorage,仍允許本次彈窗正常使用。
|
||
}
|
||
}
|
||
|
||
function clearMissingGroupDialogMarker() {
|
||
try {
|
||
sessionStorage.removeItem(MISSING_GROUP_DIALOG_KEY);
|
||
} catch {
|
||
// 不影響主要流程。
|
||
}
|
||
}
|
||
|
||
function openMissingGroupDialog() {
|
||
if (
|
||
store.isLoggedIn &&
|
||
courseStore.groupMissing &&
|
||
!hasShownMissingGroupDialog()
|
||
) {
|
||
markMissingGroupDialogShown();
|
||
missingGroupDialog.value = true;
|
||
}
|
||
}
|
||
|
||
async function goToAppointment() {
|
||
missingGroupDialog.value = false;
|
||
await router.push({ name: "appointment" });
|
||
}
|
||
|
||
async function loadFitnessGroup() {
|
||
if (!store.isLoggedIn) {
|
||
missingGroupDialog.value = false;
|
||
clearMissingGroupDialogMarker();
|
||
courseStore.resetGroup();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await courseStore.loadGroup();
|
||
clearMissingGroupDialogMarker();
|
||
} catch {
|
||
// 錯誤狀態由 course store 提供給畫面顯示與重試。
|
||
openMissingGroupDialog();
|
||
}
|
||
}
|
||
|
||
watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true });
|
||
|
||
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;
|
||
}
|
||
|
||
.score-disk {
|
||
border: 2px solid currentColor;
|
||
background-color: transparent;
|
||
width: fit-content;
|
||
min-width: 108px;
|
||
max-width: 100%;
|
||
height: 72px;
|
||
padding: 0 8px;
|
||
border-radius: 16px;
|
||
box-shadow: 0 3px 10px rgba(29, 112, 99, 0.12);
|
||
box-sizing: border-box;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.group-label {
|
||
font-size: clamp(0.95rem, 3vw, 1.1rem);
|
||
font-weight: 900;
|
||
line-height: 1;
|
||
color: inherit;
|
||
max-width: 100%;
|
||
overflow-wrap: anywhere;
|
||
text-align: center;
|
||
white-space: normal;
|
||
}
|
||
|
||
.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>
|