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,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>