Files
sports-empower-hub/app/citizen-frontend/src/pages/index.vue
T
skytek_xinliang 403d6eaa0e fix: 性別
2026-08-31 14:27:18 +08:00

455 lines
13 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>
{{ 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
? `bg-${courseStore.groupMeta.color}`
: 'bg-primary',
]"
>
<v-progress-circular
v-if="courseStore.groupLoading"
color="white"
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 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">
<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>
<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 { useAppStore } from "@/stores/app";
import { useCourseStore } from "@/stores/course";
const store = useAppStore();
const courseStore = useCourseStore();
const router = useRouter();
const missingGroupDialog = ref(false);
const displayUserName = computed(() => {
const name = store.userProfile.name || "市民";
const gender = store.userProfile.gender;
const honorific =
gender === "男" || gender === "male"
? "先生"
: gender === "女" || gender === "female"
? "女士"
: "";
return honorific ? `${name} ${honorific}` : name;
});
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: 3px solid #ffffff;
width: min(100%, 108px);
height: 72px;
padding: 0 10px;
border-radius: 22px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
flex-shrink: 0;
}
.group-label {
font-size: clamp(0.95rem, 3vw, 1.1rem);
font-weight: 900;
line-height: 1;
color: #ffffff;
white-space: nowrap;
}
.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>