feat: 新API
This commit is contained in:
@@ -127,6 +127,24 @@
|
||||
<HealthRadarChart :radar-data="store.radarData" />
|
||||
</v-card>
|
||||
|
||||
<PhysicalFitnessSummary
|
||||
:error="physicalFitnessError"
|
||||
:items="physicalFitnessItems"
|
||||
:loading="physicalFitnessLoading"
|
||||
@retry="loadPhysicalFitness"
|
||||
/>
|
||||
|
||||
<ModuleRouterCard
|
||||
clickable
|
||||
description="依目前 11 項測驗成績重新計算體適能分組。"
|
||||
icon="mdi-refresh-circle"
|
||||
icon-color="secondary"
|
||||
:disabled="generatePhysicalFitnessLoading"
|
||||
:loading="generatePhysicalFitnessLoading"
|
||||
title="重新計算體適能評級"
|
||||
@click="openGenerateDialog"
|
||||
/>
|
||||
|
||||
<ModuleRouterCard
|
||||
description="了解公益普測計畫、研究依據與平台流程。"
|
||||
icon="mdi-information-outline"
|
||||
@@ -237,6 +255,75 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-dialog v-model="generateDialog" max-width="400">
|
||||
<v-card class="rounded-xl overflow-hidden">
|
||||
<v-card-title class="bg-primary text-white font-weight-bold py-3">
|
||||
重新計算體適能評級
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<template v-if="!generateResult && !generateError">
|
||||
<v-avatar class="mb-4" color="primary" size="64">
|
||||
<v-icon color="white" icon="mdi-refresh-circle" size="34" />
|
||||
</v-avatar>
|
||||
<p class="text-body-1 font-weight-bold text-grey-darken-4 mb-2">
|
||||
確定要重新計算目前的體適能評級嗎?
|
||||
</p>
|
||||
<p class="text-body-2 text-grey-darken-2 mb-0">
|
||||
系統會依各測驗項目的最新成績重新計算分組。
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<v-alert
|
||||
v-if="generateResult"
|
||||
class="mb-0"
|
||||
:type="generateResult === '評級完成' ? 'success' : 'warning'"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ generateResult }}
|
||||
</v-alert>
|
||||
<v-alert
|
||||
v-if="generateError"
|
||||
class="mb-0"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ generateError }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4 justify-end">
|
||||
<v-btn
|
||||
:disabled="generatePhysicalFitnessLoading"
|
||||
variant="text"
|
||||
@click="closeGenerateDialog"
|
||||
>
|
||||
{{ generateResult || generateError ? "關閉" : "取消" }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="!generateResult && !generateError"
|
||||
color="primary"
|
||||
:loading="generatePhysicalFitnessLoading"
|
||||
variant="flat"
|
||||
@click="handleGeneratePhysicalFitness"
|
||||
>
|
||||
確認重新計算
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="generateError"
|
||||
color="primary"
|
||||
:loading="generatePhysicalFitnessLoading"
|
||||
variant="flat"
|
||||
@click="handleGeneratePhysicalFitness"
|
||||
>
|
||||
重試
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="missingGroupDialog" max-width="400">
|
||||
<v-card class="rounded-xl overflow-hidden text-center">
|
||||
<v-card-title class="bg-primary text-white font-weight-bold py-3">
|
||||
@@ -285,10 +372,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import type { PhysicalFitnessItem } from "@/api/types";
|
||||
import {
|
||||
fetchPhysicalFitness,
|
||||
generatePhysicalFitness,
|
||||
} from "@/api/physical-fitness";
|
||||
import BrandAvatar from "@/components/BrandAvatar.vue";
|
||||
import HealthRadarChart from "@/components/HealthRadarChart.vue";
|
||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
||||
import ModuleRouterCard from "@/components/ModuleRouterCard.vue";
|
||||
import PhysicalFitnessSummary from "@/components/PhysicalFitnessSummary.vue";
|
||||
import { useDisplayUserName } from "@/composables/useDisplayUserName";
|
||||
import {
|
||||
APPOINTMENT_ENABLED,
|
||||
@@ -297,11 +390,20 @@ import {
|
||||
} from "@/config/features";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCourseStore } from "@/stores/course";
|
||||
import { getApiErrorMessage } from "@/utils/api-error";
|
||||
|
||||
const store = useAppStore();
|
||||
const courseStore = useCourseStore();
|
||||
const router = useRouter();
|
||||
const missingGroupDialog = ref(false);
|
||||
const generateDialog = ref(false);
|
||||
const generatePhysicalFitnessLoading = ref(false);
|
||||
const generateResult = ref("");
|
||||
const generateError = ref("");
|
||||
const physicalFitnessItems = ref<PhysicalFitnessItem[]>([]);
|
||||
const physicalFitnessLoading = ref(false);
|
||||
const physicalFitnessError = ref("");
|
||||
const redirectingToLogin = ref(false);
|
||||
|
||||
const displayUserName = useDisplayUserName(store.userProfile);
|
||||
|
||||
@@ -364,7 +466,99 @@ async function loadFitnessGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
function getRequiredUserID(): string | null {
|
||||
const userID = store.userID.trim();
|
||||
|
||||
if (userID) {
|
||||
return userID;
|
||||
}
|
||||
|
||||
if (!redirectingToLogin.value) {
|
||||
redirectingToLogin.value = true;
|
||||
void router.replace({ name: "login", query: { redirect: "/" } });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadPhysicalFitness() {
|
||||
if (!store.isLoggedIn) {
|
||||
physicalFitnessItems.value = [];
|
||||
physicalFitnessError.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const userID = getRequiredUserID();
|
||||
if (!userID) {
|
||||
return;
|
||||
}
|
||||
|
||||
physicalFitnessLoading.value = true;
|
||||
physicalFitnessError.value = "";
|
||||
|
||||
try {
|
||||
physicalFitnessItems.value = await fetchPhysicalFitness(userID);
|
||||
} catch (error) {
|
||||
physicalFitnessError.value = getApiErrorMessage(
|
||||
error,
|
||||
"體適能測驗資料載入失敗,請稍後再試。",
|
||||
);
|
||||
} finally {
|
||||
physicalFitnessLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openGenerateDialog() {
|
||||
if (!getRequiredUserID()) {
|
||||
return;
|
||||
}
|
||||
|
||||
generateResult.value = "";
|
||||
generateError.value = "";
|
||||
generateDialog.value = true;
|
||||
}
|
||||
|
||||
function closeGenerateDialog() {
|
||||
if (generatePhysicalFitnessLoading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
generateDialog.value = false;
|
||||
}
|
||||
|
||||
async function handleGeneratePhysicalFitness() {
|
||||
const userID = getRequiredUserID();
|
||||
if (!userID) {
|
||||
return;
|
||||
}
|
||||
|
||||
generatePhysicalFitnessLoading.value = true;
|
||||
generateResult.value = "";
|
||||
generateError.value = "";
|
||||
|
||||
try {
|
||||
const response = await generatePhysicalFitness(userID);
|
||||
generateResult.value = response.message;
|
||||
|
||||
if (response.message === "評級完成") {
|
||||
await courseStore.loadGroup(true);
|
||||
}
|
||||
|
||||
await loadPhysicalFitness();
|
||||
} catch (error) {
|
||||
generateError.value = getApiErrorMessage(
|
||||
error,
|
||||
"體適能評級重新計算失敗,請稍後再試。",
|
||||
);
|
||||
} finally {
|
||||
generatePhysicalFitnessLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true });
|
||||
watch([() => store.isLoggedIn, () => store.userID], loadPhysicalFitness, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const todayTimeString = computed(() => {
|
||||
const now = new Date();
|
||||
|
||||
Reference in New Issue
Block a user