feat: bmi
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
BodyCompositionResponse,
|
||||||
GeneratePhysicalFitnessResponse,
|
GeneratePhysicalFitnessResponse,
|
||||||
PhysicalFitnessResponse,
|
PhysicalFitnessResponse,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -21,3 +22,12 @@ export function generatePhysicalFitness(
|
|||||||
.post(`v1/physical-fitness/generate/${encodeURIComponent(userID)}`)
|
.post(`v1/physical-fitness/generate/${encodeURIComponent(userID)}`)
|
||||||
.json<GeneratePhysicalFitnessResponse>();
|
.json<GeneratePhysicalFitnessResponse>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 取得指定使用者的身體組成分析。 */
|
||||||
|
export function fetchBodyComposition(
|
||||||
|
userCode: string,
|
||||||
|
): Promise<BodyCompositionResponse> {
|
||||||
|
return api
|
||||||
|
.get(`v1/physical-fitness/body-composition/${encodeURIComponent(userCode)}`)
|
||||||
|
.json<BodyCompositionResponse>();
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ export interface GeneratePhysicalFitnessResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** PhysicalFitnessController 回傳的身體組成分析。 */
|
||||||
|
export interface BodyCompositionResponse {
|
||||||
|
heightCm: number;
|
||||||
|
weightKg: number;
|
||||||
|
bmi: number;
|
||||||
|
bmiStatus: string;
|
||||||
|
bodyFatPercent: number;
|
||||||
|
bodyFatRange: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 性別 */
|
/** 性別 */
|
||||||
export type Gender = "male" | "female";
|
export type Gender = "male" | "female";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { WHO_BMI_STANDARDS } from "@/data/whoBmiStandards";
|
||||||
|
import {
|
||||||
|
BODY_COMPOSITION_COLORS,
|
||||||
|
getBmiTone,
|
||||||
|
} from "@/utils/body-composition-status";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
weightKg: number;
|
||||||
|
bmi: number;
|
||||||
|
bmiStatus: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const statusColor = computed(
|
||||||
|
() => BODY_COMPOSITION_COLORS[getBmiTone(props.bmi)],
|
||||||
|
);
|
||||||
|
|
||||||
|
const recommendation = computed(() => {
|
||||||
|
if (props.bmi < 18.5) {
|
||||||
|
return "體重低於正常範圍,建議留意營養攝取與健康狀況。";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.bmi < 25) {
|
||||||
|
return "體重位於正常範圍,請持續維持均衡飲食與規律運動。";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.bmi < 30) {
|
||||||
|
return "體重已超過正常範圍,建議適度調整體重。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "您的 BMI 已進入肥胖範圍,建議諮詢專業人員並逐步調整生活習慣。";
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatMeasurement(value: number): string {
|
||||||
|
return Number.isFinite(value) ? value.toFixed(1) : "--";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="analysis-card rounded-xl" elevation="1">
|
||||||
|
<v-card-title class="text-h5 font-weight-black text-center pt-6">
|
||||||
|
體重與 BMI 分析
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4 pa-sm-6">
|
||||||
|
<div class="metrics-grid mb-5">
|
||||||
|
<div class="metric-cell text-center">
|
||||||
|
<div class="text-body-large text-grey-darken-2 mb-2">體重</div>
|
||||||
|
<div class="metric-value text-grey-darken-4">
|
||||||
|
{{ formatMeasurement(weightKg) }}
|
||||||
|
<span class="metric-unit">公斤</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-cell text-center">
|
||||||
|
<div class="text-body-large text-grey-darken-2 mb-2">BMI</div>
|
||||||
|
<div class="metric-value" :style="{ color: statusColor }">
|
||||||
|
{{ formatMeasurement(bmi) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap justify-center align-center ga-2 mb-5">
|
||||||
|
<span class="text-title-medium">您的 BMI 屬於</span>
|
||||||
|
<v-chip
|
||||||
|
class="semantic-chip font-weight-black"
|
||||||
|
:color="statusColor"
|
||||||
|
label
|
||||||
|
size="large"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{{ bmiStatus }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
class="recommendation-alert mb-6"
|
||||||
|
:color="statusColor"
|
||||||
|
icon="mdi-bullhorn-outline"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ recommendation }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-sheet class="who-panel rounded-xl pa-4" color="grey-lighten-4">
|
||||||
|
<div class="who-title text-center mb-1">世界衛生組織 WHO 定義</div>
|
||||||
|
<div class="who-formula text-grey-darken-2 text-center mb-5">
|
||||||
|
(BMI = 體重 ÷ 身高²)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="who-standards">
|
||||||
|
<div
|
||||||
|
v-for="standard in WHO_BMI_STANDARDS"
|
||||||
|
:key="standard.label"
|
||||||
|
class="who-standard rounded-lg text-center"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: BODY_COMPOSITION_COLORS[standard.tone],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="who-standard-label">{{ standard.label }}</div>
|
||||||
|
<div class="who-standard-range">{{ standard.range }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="who-note text-grey-darken-2 text-center mb-0 mt-5">
|
||||||
|
BMI 只是初步判斷,需搭配體脂率<br
|
||||||
|
class="d-none d-sm-inline"
|
||||||
|
/>與肌肉量進一步分析。
|
||||||
|
</p>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.analysis-card {
|
||||||
|
border: 1px solid rgba(var(--v-theme-primary), 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell + .metric-cell {
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: clamp(2.25rem, 10vw, 3.125rem);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-unit {
|
||||||
|
font-size: clamp(0.875rem, 3.5vw, 1.25rem);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-alert {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semantic-chip {
|
||||||
|
color: #252525 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.who-title {
|
||||||
|
font-size: clamp(1.25rem, 5vw, 1.65rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.who-formula,
|
||||||
|
.who-note {
|
||||||
|
font-size: clamp(1rem, 4vw, 1.25rem);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.who-standards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.who-standard {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 18px 6px 16px;
|
||||||
|
color: #2b2b2b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.who-standard-label {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: clamp(1.2rem, 5vw, 1.55rem);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.who-standard-range {
|
||||||
|
font-size: clamp(0.92rem, 3.8vw, 1.12rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.25;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.metric-cell {
|
||||||
|
padding-inline: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.who-panel {
|
||||||
|
padding-inline: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.who-standards {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import {
|
||||||
|
BODY_COMPOSITION_COLORS,
|
||||||
|
getBodyFatTone,
|
||||||
|
} from "@/utils/body-composition-status";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
bodyFatPercent: number;
|
||||||
|
bodyFatRange: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const statusColor = computed(
|
||||||
|
() => BODY_COMPOSITION_COLORS[getBodyFatTone(props.bodyFatRange)],
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedBodyFatPercent = computed(() =>
|
||||||
|
Number.isFinite(props.bodyFatPercent)
|
||||||
|
? props.bodyFatPercent.toFixed(1)
|
||||||
|
: "--",
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="analysis-card rounded-xl mt-4" elevation="1">
|
||||||
|
<v-card-title class="text-h5 font-weight-black text-center pt-6">
|
||||||
|
體脂率分析
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="text-center pa-6">
|
||||||
|
<div class="text-title-medium text-grey-darken-2 mb-2">體脂率</div>
|
||||||
|
<div class="body-fat-value mb-6" :style="{ color: statusColor }">
|
||||||
|
{{ formattedBodyFatPercent }}<span class="body-fat-unit">%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap justify-center align-center ga-2">
|
||||||
|
<span class="text-title-medium">您的體脂率屬於</span>
|
||||||
|
<v-chip
|
||||||
|
class="semantic-chip font-weight-black"
|
||||||
|
:color="statusColor"
|
||||||
|
label
|
||||||
|
size="large"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{{ bodyFatRange }}
|
||||||
|
</v-chip>
|
||||||
|
<span class="text-title-medium">範圍</span>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.analysis-card {
|
||||||
|
border: 1px solid rgba(var(--v-theme-primary), 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-fat-value {
|
||||||
|
font-size: clamp(2.25rem, 10vw, 3.125rem);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semantic-chip {
|
||||||
|
color: #252525 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-fat-unit {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: clamp(1.25rem, 5vw, 2rem);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -109,6 +109,15 @@
|
|||||||
@click="drawer = false"
|
@click="drawer = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-if="store.isLoggedIn"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-scale-bathroom"
|
||||||
|
title="身體數值分析"
|
||||||
|
to="/body-composition"
|
||||||
|
@click="drawer = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<v-list-item
|
<v-list-item
|
||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="mdi-information-outline"
|
prepend-icon="mdi-information-outline"
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title: string;
|
||||||
|
backTo?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
backTo: "/",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="page-heading d-flex align-center mb-2">
|
||||||
|
<v-btn
|
||||||
|
aria-label="返回首頁"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-arrow-left"
|
||||||
|
rounded="pill"
|
||||||
|
:to="backTo"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</v-btn>
|
||||||
|
<h1 class="page-title text-primary">{{ title }}</h1>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-heading {
|
||||||
|
position: relative;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
width: min(60%, 320px);
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.25rem, 5vw, 1.75rem);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.page-title {
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
margin-left: 12px;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export const APPOINTMENT_ENABLED = false
|
export const APPOINTMENT_ENABLED = false;
|
||||||
export const BOTTOM_NAVIGATION_ENABLED = false
|
export const BOTTOM_NAVIGATION_ENABLED = false;
|
||||||
export const CHECKIN_ENABLED = false
|
export const CHECKIN_ENABLED = false;
|
||||||
export const EXERCISE_DURATION_RECOMMENDATION_ENABLED = false
|
export const EXERCISE_DURATION_RECOMMENDATION_ENABLED = false;
|
||||||
export const RADAR_ENABLED = false
|
export const RADAR_ENABLED = false;
|
||||||
export const SELF_ASSESSMENT_ENABLED = false
|
export const SELF_ASSESSMENT_ENABLED = false;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "正常",
|
||||||
|
"range": "18.5–24.9",
|
||||||
|
"tone": "normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "過重",
|
||||||
|
"range": "25.0–29.9",
|
||||||
|
"tone": "caution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "肥胖",
|
||||||
|
"range": "30.0以上",
|
||||||
|
"tone": "danger"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import whoBmiStandards from "./who-bmi-standards.json";
|
||||||
|
import type { BodyCompositionTone } from "@/utils/body-composition-status";
|
||||||
|
|
||||||
|
export interface WhoBmiStandard {
|
||||||
|
label: string;
|
||||||
|
range: string;
|
||||||
|
tone: BodyCompositionTone;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WHO_BMI_STANDARDS = whoBmiStandards as WhoBmiStandard[];
|
||||||
@@ -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,19 +2,7 @@
|
|||||||
<Layout>
|
<Layout>
|
||||||
<template #main>
|
<template #main>
|
||||||
<template v-if="store.groupLoaded">
|
<template v-if="store.groupLoaded">
|
||||||
<!-- 頁面標題:刻意縮小,把版面讓給下方的組別 -->
|
<PageHeading title="課程建議" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 體適能組別:本頁所有內容都依它而定,因此獨立成一張顯眼的卡片 -->
|
<!-- 體適能組別:本頁所有內容都依它而定,因此獨立成一張顯眼的卡片 -->
|
||||||
<v-card
|
<v-card
|
||||||
@@ -401,6 +389,7 @@
|
|||||||
import type { TimeSlot, YesNo } from "@/api/types";
|
import type { TimeSlot, YesNo } from "@/api/types";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
import Layout from "@/components/layout/DefaultLayout.vue";
|
||||||
|
import PageHeading from "@/components/layout/PageHeading.vue";
|
||||||
import { EXERCISE_DURATION_RECOMMENDATION_ENABLED } from "@/config/features";
|
import { EXERCISE_DURATION_RECOMMENDATION_ENABLED } from "@/config/features";
|
||||||
import { COURSE_DETAIL } from "@/data/courseDetail";
|
import { COURSE_DETAIL } from "@/data/courseDetail";
|
||||||
import { useCourseStore } from "@/stores/course";
|
import { useCourseStore } from "@/stores/course";
|
||||||
|
|||||||
@@ -104,6 +104,14 @@
|
|||||||
title="重新計算體適能評級"
|
title="重新計算體適能評級"
|
||||||
@click="openGenerateDialog"
|
@click="openGenerateDialog"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ModuleRouterCard
|
||||||
|
description="查看體重、BMI 與體脂率的個人分析結果。"
|
||||||
|
icon="mdi-scale-bathroom"
|
||||||
|
icon-color="primary"
|
||||||
|
title="身體數值分析"
|
||||||
|
to="/body-composition"
|
||||||
|
/>
|
||||||
</div>
|
</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 fitnessResearchImage from "@/assets/introduction/expo_park_open/open_02.jpg";
|
||||||
import expoParkZonesImage from "@/assets/introduction/expo_park_open/open_03.jpg";
|
import expoParkZonesImage from "@/assets/introduction/expo_park_open/open_03.jpg";
|
||||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
import Layout from "@/components/layout/DefaultLayout.vue";
|
||||||
|
import PageHeading from "@/components/layout/PageHeading.vue";
|
||||||
|
|
||||||
const slides = [
|
const slides = [
|
||||||
{
|
{
|
||||||
@@ -31,14 +32,9 @@ const activeSlide = shallowRef(0);
|
|||||||
<template>
|
<template>
|
||||||
<Layout>
|
<Layout>
|
||||||
<template #main>
|
<template #main>
|
||||||
<v-card variant="flat">
|
<PageHeading title="計畫簡介" />
|
||||||
<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>
|
|
||||||
|
|
||||||
|
<v-card variant="flat">
|
||||||
<div class="carousel-shell">
|
<div class="carousel-shell">
|
||||||
<v-carousel
|
<v-carousel
|
||||||
v-model="activeSlide"
|
v-model="activeSlide"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import Appointment from "@/pages/appointment.vue";
|
import Appointment from "@/pages/appointment.vue";
|
||||||
|
import BodyComposition from "@/pages/body-composition.vue";
|
||||||
import Checkin from "@/pages/checkin.vue";
|
import Checkin from "@/pages/checkin.vue";
|
||||||
import CheckinDetail from "@/pages/checkinDetail.vue";
|
import CheckinDetail from "@/pages/checkinDetail.vue";
|
||||||
import Course from "@/pages/course.vue";
|
import Course from "@/pages/course.vue";
|
||||||
@@ -53,6 +54,16 @@ const router = createRouter({
|
|||||||
component: Profile,
|
component: Profile,
|
||||||
meta: { requiresAuth: true, title: "個人資料修改|運動玩轉健康力" },
|
meta: { requiresAuth: true, title: "個人資料修改|運動玩轉健康力" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/body-composition",
|
||||||
|
name: "body-composition",
|
||||||
|
component: BodyComposition,
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresUserID: true,
|
||||||
|
title: "身體數值分析|運動玩轉健康力",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/appointment",
|
path: "/appointment",
|
||||||
name: "appointment",
|
name: "appointment",
|
||||||
@@ -106,6 +117,13 @@ router.beforeEach(async (to) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (to.meta.requiresUserID && !store.userID.trim()) {
|
||||||
|
return {
|
||||||
|
name: "login",
|
||||||
|
query: { redirect: to.fullPath },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (to.meta.requiresFitnessGroup) {
|
if (to.meta.requiresFitnessGroup) {
|
||||||
const courseStore = useCourseStore();
|
const courseStore = useCourseStore();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export type BodyCompositionTone = "normal" | "caution" | "danger";
|
||||||
|
|
||||||
|
export const BODY_COMPOSITION_COLORS: Record<BodyCompositionTone, string> = {
|
||||||
|
normal: "#9ac36d",
|
||||||
|
caution: "#fbd051",
|
||||||
|
danger: "#ef6b6b",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getBmiTone(bmi: number): BodyCompositionTone {
|
||||||
|
if (bmi >= 18.5 && bmi < 25) return "normal";
|
||||||
|
if (bmi >= 30) return "danger";
|
||||||
|
return "caution";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBodyFatTone(bodyFatRange: string): BodyCompositionTone {
|
||||||
|
if (/肥胖|過高|高危|危險/.test(bodyFatRange)) return "danger";
|
||||||
|
if (/標準|健康|正常/.test(bodyFatRange)) return "normal";
|
||||||
|
return "caution";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user