Files
sports-empower-hub/app/citizen-frontend/src/components/PhysicalFitnessSummary.vue
T
2026-09-16 09:29:54 +08:00

154 lines
4.3 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>
<v-card class="rounded-xl elevation-2 mt-4" color="surface">
<v-card-title class="d-flex align-center justify-space-between">
<span>11 項體適能測驗</span>
<div class="d-flex align-center ga-2">
<v-btn
aria-label="重新取得體適能資料"
color="primary"
:disabled="loading"
icon="mdi-refresh"
:loading="loading"
size="small"
variant="text"
@click="$emit('refresh')"
/>
<v-chip color="primary" size="small" variant="tonal">
{{ completedCount }}/{{ displayItems.length }} 項有資料
</v-chip>
</div>
</v-card-title>
<v-card-subtitle> 最後量測時間:{{ formattedReceivedAt }} </v-card-subtitle>
<v-card-text>
<v-progress-linear
v-if="loading"
class="mb-4"
color="primary"
indeterminate
rounded
/>
<v-alert
v-if="error"
class="mb-4"
density="compact"
type="error"
variant="tonal"
>
{{ error }}
<v-btn
class="ml-2"
color="error"
size="small"
variant="text"
@click="$emit('refresh')"
>
重試
</v-btn>
</v-alert>
<v-row density="compact">
<v-col
v-for="item in displayItems"
:key="item.fitnessCode"
cols="12"
sm="6"
md="4"
>
<div
class="test-item d-flex align-center justify-space-between rounded-lg px-3"
>
<div class="mr-3">
<div class="text-body-2 font-weight-bold">
{{ item.fitnessName }}
</div>
</div>
<div
class="text-body-1 font-weight-black text-no-wrap"
:class="item.fitnessScore === null ? 'text-grey' : 'text-primary'"
>
{{ item.fitnessScore ?? "無資料" }}
</div>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
import { computed } from "vue";
import type { PhysicalFitnessItem } from "@/api/types";
const props = defineProps<{
error: string;
items: PhysicalFitnessItem[];
loading: boolean;
receivedAt: string | null;
}>();
defineEmits<{
refresh: [];
}>();
const FITNESS_TEST_DEFINITIONS = [
{ code: "UPGO_244M", name: "八英呎起走(秒)", aliases: [] },
{ code: "SIT_STAND_30S", name: "30 秒坐站(次)", aliases: ["SIT30S"] },
{ code: "SIT_REACH_L", name: "坐姿體前彎左側(公分)", aliases: [] },
{ code: "SIT_REACH_R", name: "坐姿體前彎右側(公分)", aliases: [] },
{ code: "ARM_CURL_L", name: "30 秒手臂彎舉左側(次)", aliases: [] },
{ code: "ARM_CURL_R", name: "30 秒手臂彎舉右側(次)", aliases: [] },
{ code: "BACK_SCRATCH_L", name: "抓背測驗左側(公分)", aliases: [] },
{ code: "BACK_SCRATCH_R", name: "抓背測驗右側(公分)", aliases: [] },
{ code: "ONE_LEG_STAND_L", name: "單腳站立左側(秒)", aliases: [] },
{ code: "ONE_LEG_STAND_R", name: "單腳站立右側(秒)", aliases: [] },
{ code: "STEP_TEST_30S", name: "30 秒抬膝踏步(次)", aliases: [] },
] as const;
const displayItems = computed(() =>
FITNESS_TEST_DEFINITIONS.map((definition) => {
const item = props.items.find(
(candidate) =>
candidate.fitnessCode === definition.code ||
definition.aliases.some((alias) => alias === candidate.fitnessCode),
);
return {
fitnessCode: definition.code,
fitnessName: item?.fitnessName || definition.name,
fitnessScore: item?.fitnessScore ?? null,
};
}),
);
const completedCount = computed(
() => displayItems.value.filter((item) => item.fitnessScore !== null).length,
);
const formattedReceivedAt = computed(() => {
if (!props.receivedAt) {
return "無資料";
}
const receivedAt = new Date(props.receivedAt);
if (Number.isNaN(receivedAt.getTime())) {
return "無資料";
}
return new Intl.DateTimeFormat("zh-TW", {
dateStyle: "medium",
timeStyle: "short",
}).format(receivedAt);
});
</script>
<style scoped>
.test-item {
min-height: 48px;
border: 1px solid rgba(0, 0, 0, 0.08);
background: rgba(var(--v-theme-primary), 0.03);
}
</style>