154 lines
4.3 KiB
Vue
154 lines
4.3 KiB
Vue
<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>
|