feat: bmi

This commit is contained in:
skytek_xinliang
2026-09-18 09:58:52 +08:00
parent ea8793e34c
commit 0f3e7b8a56
15 changed files with 534 additions and 26 deletions
@@ -0,0 +1,87 @@
<script setup lang="ts">
import { onMounted, shallowRef } from "vue";
import { useRouter } from "vue-router";
import type { BodyCompositionResponse } from "@/api/types";
import { fetchBodyComposition } from "@/api/physical-fitness";
import BmiAnalysisCard from "@/components/body-composition/BmiAnalysisCard.vue";
import BodyFatAnalysisCard from "@/components/body-composition/BodyFatAnalysisCard.vue";
import Layout from "@/components/layout/DefaultLayout.vue";
import PageHeading from "@/components/layout/PageHeading.vue";
import { useAppStore } from "@/stores/app";
import { getApiErrorMessage } from "@/utils/api-error";
const store = useAppStore();
const router = useRouter();
const bodyComposition = shallowRef<BodyCompositionResponse | null>(null);
const loading = shallowRef(false);
const errorMessage = shallowRef("");
async function loadBodyComposition() {
const userCode = store.userID.trim();
if (!userCode) {
await router.replace({
name: "login",
query: { redirect: "/body-composition" },
});
return;
}
loading.value = true;
errorMessage.value = "";
try {
bodyComposition.value = await fetchBodyComposition(userCode);
} catch (error) {
errorMessage.value = getApiErrorMessage(
error,
"身體數值資料載入失敗,請稍後再試。",
);
} finally {
loading.value = false;
}
}
onMounted(loadBodyComposition);
</script>
<template>
<Layout>
<template #main>
<div class="max-width-mobile pb-2">
<PageHeading title="身體數值分析" />
<template v-if="loading">
<v-skeleton-loader class="rounded-xl mb-4" type="article, table" />
<v-skeleton-loader class="rounded-xl" type="article" />
</template>
<v-alert
v-else-if="errorMessage"
class="rounded-xl"
icon="mdi-alert-circle-outline"
type="error"
variant="tonal"
>
{{ errorMessage }}
<template #append>
<v-btn size="small" variant="text" @click="loadBodyComposition">
重試
</v-btn>
</template>
</v-alert>
<template v-else-if="bodyComposition">
<BmiAnalysisCard
:bmi="bodyComposition.bmi"
:bmi-status="bodyComposition.bmiStatus"
:weight-kg="bodyComposition.weightKg"
/>
<BodyFatAnalysisCard
:body-fat-percent="bodyComposition.bodyFatPercent"
:body-fat-range="bodyComposition.bodyFatRange"
/>
</template>
</div>
</template>
</Layout>
</template>
+2 -13
View File
@@ -2,19 +2,7 @@
<Layout>
<template #main>
<template v-if="store.groupLoaded">
<!-- 頁面標題:刻意縮小,把版面讓給下方的組別 -->
<div class="d-flex align-center mb-3">
<v-icon
class="mr-2"
color="primary"
icon="mdi-calendar-week"
size="18"
/>
<span class="text-title-small font-weight-bold text-grey-darken-1"
>課程建議</span
>
</div>
<PageHeading title="課程建議" />
<!-- 體適能組別:本頁所有內容都依它而定,因此獨立成一張顯眼的卡片 -->
<v-card
@@ -401,6 +389,7 @@
import type { TimeSlot, YesNo } from "@/api/types";
import { computed, onMounted, ref } from "vue";
import Layout from "@/components/layout/DefaultLayout.vue";
import PageHeading from "@/components/layout/PageHeading.vue";
import { EXERCISE_DURATION_RECOMMENDATION_ENABLED } from "@/config/features";
import { COURSE_DETAIL } from "@/data/courseDetail";
import { useCourseStore } from "@/stores/course";
+8
View File
@@ -104,6 +104,14 @@
title="重新計算體適能評級"
@click="openGenerateDialog"
/>
<ModuleRouterCard
description="查看體重、BMI 與體脂率的個人分析結果。"
icon="mdi-scale-bathroom"
icon-color="primary"
title="身體數值分析"
to="/body-composition"
/>
</div>
<!-- 課程建議入口 -->
@@ -5,6 +5,7 @@ import sportsEmpowerHubImage from "@/assets/introduction/expo_park_open/open_01.
import fitnessResearchImage from "@/assets/introduction/expo_park_open/open_02.jpg";
import expoParkZonesImage from "@/assets/introduction/expo_park_open/open_03.jpg";
import Layout from "@/components/layout/DefaultLayout.vue";
import PageHeading from "@/components/layout/PageHeading.vue";
const slides = [
{
@@ -31,14 +32,9 @@ const activeSlide = shallowRef(0);
<template>
<Layout>
<template #main>
<v-card variant="flat">
<v-card-title
class="font-weight-bold text-primary d-flex align-center py-0"
>
<v-icon class="mr-4" color="accent" icon="mdi-information-outline" />
計畫簡介
</v-card-title>
<PageHeading title="計畫簡介" />
<v-card variant="flat">
<div class="carousel-shell">
<v-carousel
v-model="activeSlide"