fix: format

This commit is contained in:
skytek_xinliang
2026-10-05 10:10:57 +08:00
parent 4956885ecc
commit a8fdb157a2
5 changed files with 288 additions and 170 deletions
+16
View File
@@ -0,0 +1,16 @@
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": [
"-y",
"chrome-devtools-mcp@latest",
"--browserUrl",
"http://localhost:9222"
]
},
"vuetify-mcp": {
"url": "https://mcp.vuetifyjs.com/mcp"
}
}
}
@@ -1,20 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
const phone = defineModel<string>({ required: true }) const phone = defineModel<string>({ required: true });
defineProps<{ defineProps<{
errorMessage: string errorMessage: string;
loading: boolean loading: boolean;
retryAfterSeconds: number retryAfterSeconds: number;
}>() }>();
const emit = defineEmits<{ const emit = defineEmits<{
submit: [] submit: [];
}>() }>();
</script> </script>
<template> <template>
<v-form @submit.prevent="emit('submit')"> <v-form @submit.prevent="emit('submit')">
<v-alert v-if="errorMessage" class="mb-4" density="compact" type="error" variant="tonal"> <v-alert
v-if="errorMessage"
class="mb-4"
density="compact"
type="error"
variant="tonal"
>
{{ errorMessage }} {{ errorMessage }}
</v-alert> </v-alert>
@@ -41,7 +47,7 @@ const emit = defineEmits<{
:loading="loading" :loading="loading"
type="submit" type="submit"
> >
{{ retryAfterSeconds > 0 ? `${retryAfterSeconds} 秒後再試` : '下一步' }} {{ retryAfterSeconds > 0 ? `${retryAfterSeconds} 秒後再試` : "下一步" }}
</v-btn> </v-btn>
</v-form> </v-form>
</template> </template>
@@ -6,7 +6,9 @@
<v-app-bar-nav-icon variant="text" @click="drawer = !drawer" /> <v-app-bar-nav-icon variant="text" @click="drawer = !drawer" />
</template> </template>
<v-app-bar-title class="font-weight-bold flex-grow-1"> 運動玩轉健康力 </v-app-bar-title> <v-app-bar-title class="font-weight-bold flex-grow-1">
運動玩轉健康力
</v-app-bar-title>
<template #append> <template #append>
<div class="d-flex align-center"> <div class="d-flex align-center">
@@ -19,7 +21,10 @@
variant="text" variant="text"
@click="isLargeText = !isLargeText" @click="isLargeText = !isLargeText"
> >
<v-icon :color="isLargeText ? 'accent' : 'white'" icon="mdi-format-size" /> <v-icon
:color="isLargeText ? 'accent' : 'white'"
icon="mdi-format-size"
/>
</v-btn> </v-btn>
</div> </div>
</template> </template>
@@ -61,7 +66,9 @@
</v-avatar> </v-avatar>
</template> </template>
<v-list-item-subtitle class="text-white-50">未登入訪客</v-list-item-subtitle> <v-list-item-subtitle class="text-white-50"
>未登入訪客</v-list-item-subtitle
>
</v-list-item> </v-list-item>
<v-divider /> <v-divider />
@@ -74,7 +81,12 @@
<v-list-item-title>大字體模式</v-list-item-title> <v-list-item-title>大字體模式</v-list-item-title>
<template #append> <template #append>
<v-switch v-model="isLargeText" color="secondary" density="compact" hide-details /> <v-switch
v-model="isLargeText"
color="secondary"
density="compact"
hide-details
/>
</template> </template>
</v-list-item> </v-list-item>
@@ -191,11 +203,16 @@
</v-navigation-drawer> </v-navigation-drawer>
<!-- 主要內容區 --> <!-- 主要內容區 -->
<v-main class="bg-white min-vh-100" :class="{ 'pb-16': BOTTOM_NAVIGATION_ENABLED }"> <v-main
class="bg-white min-vh-100"
:class="{ 'pb-16': BOTTOM_NAVIGATION_ENABLED }"
>
<v-container class="px-4 py-2"> <v-container class="px-4 py-2">
<v-alert <v-alert
v-if=" v-if="
store.isLoggedIn && store.requiresEmailCompletion && route.name !== 'complete-email' store.isLoggedIn &&
store.requiresEmailCompletion &&
route.name !== 'complete-email'
" "
class="mb-2" class="mb-2"
color="warning" color="warning"
@@ -206,11 +223,16 @@
<span class="text-title-small"> <span class="text-title-small">
{{ {{
EMAIL_COMPLETION_REQUIRED EMAIL_COMPLETION_REQUIRED
? '請先補登 E-Mail,才能繼續使用平台。' ? "請先補登 E-Mail,才能繼續使用平台。"
: '尚未設定 E-Mail,前往補登以接收重要通知。' : "尚未設定 E-Mail,前往補登以接收重要通知。"
}} }}
</span> </span>
<v-btn color="warning" size="small" :to="emailCompletionLocation" variant="flat"> <v-btn
color="warning"
size="small"
:to="emailCompletionLocation"
variant="flat"
>
前往補登 前往補登
</v-btn> </v-btn>
</div> </div>
@@ -230,7 +252,9 @@
tag="div" tag="div"
> >
<v-btn to="/" value="home"> <v-btn to="/" value="home">
<v-icon :color="getBottomNavIconColor('home')">mdi-view-dashboard</v-icon> <v-icon :color="getBottomNavIconColor('home')"
>mdi-view-dashboard</v-icon
>
<span class="bottom-nav-text">首頁</span> <span class="bottom-nav-text">首頁</span>
</v-btn> </v-btn>
@@ -246,13 +270,25 @@
<span class="bottom-nav-text">自評</span> <span class="bottom-nav-text">自評</span>
</v-btn> </v-btn>
<v-btn v-if="CHECKIN_ENABLED && store.isLoggedIn" to="/checkin" value="checkin"> <v-btn
<v-icon :color="getBottomNavIconColor('checkin')">mdi-qrcode-scan</v-icon> v-if="CHECKIN_ENABLED && store.isLoggedIn"
to="/checkin"
value="checkin"
>
<v-icon :color="getBottomNavIconColor('checkin')"
>mdi-qrcode-scan</v-icon
>
<span class="bottom-nav-text">量測</span> <span class="bottom-nav-text">量測</span>
</v-btn> </v-btn>
<v-btn v-if="APPOINTMENT_ENABLED && store.isLoggedIn" to="/appointment" value="appointment"> <v-btn
<v-icon :color="getBottomNavIconColor('appointment')">mdi-calendar-check</v-icon> v-if="APPOINTMENT_ENABLED && store.isLoggedIn"
to="/appointment"
value="appointment"
>
<v-icon :color="getBottomNavIconColor('appointment')"
>mdi-calendar-check</v-icon
>
<span class="bottom-nav-text">預約</span> <span class="bottom-nav-text">預約</span>
</v-btn> </v-btn>
</v-bottom-navigation> </v-bottom-navigation>
@@ -260,7 +296,9 @@
<!-- 隱私授權對話框 --> <!-- 隱私授權對話框 -->
<v-dialog v-model="showPrivacy" max-width="500"> <v-dialog v-model="showPrivacy" max-width="500">
<v-card class="rounded-xl"> <v-card class="rounded-xl">
<v-card-title class="bg-primary text-white font-weight-bold d-flex align-center"> <v-card-title
class="bg-primary text-white font-weight-bold d-flex align-center"
>
<v-icon class="mr-2" icon="mdi-shield-check" /> <v-icon class="mr-2" icon="mdi-shield-check" />
個資同意與資料授權 個資同意與資料授權
</v-card-title> </v-card-title>
@@ -293,8 +331,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from "vue-router";
import { import {
APPOINTMENT_ENABLED, APPOINTMENT_ENABLED,
BODY_COMPOSITION_ENABLED, BODY_COMPOSITION_ENABLED,
@@ -302,53 +340,54 @@ import {
CHECKIN_ENABLED, CHECKIN_ENABLED,
EMAIL_COMPLETION_REQUIRED, EMAIL_COMPLETION_REQUIRED,
SELF_ASSESSMENT_ENABLED, SELF_ASSESSMENT_ENABLED,
} from '@/config/features' } from "@/config/features";
import { useAppStore } from '@/stores/app' import { useAppStore } from "@/stores/app";
import { useCourseStore } from '@/stores/course' import { useCourseStore } from "@/stores/course";
const store = useAppStore() const store = useAppStore();
const courseStore = useCourseStore() const courseStore = useCourseStore();
const router = useRouter() const router = useRouter();
const route = useRoute() const route = useRoute();
const drawer = ref(false) const drawer = ref(false);
const activeTab = ref('home') const activeTab = ref("home");
const showPrivacy = ref(false) const showPrivacy = ref(false);
const emailCompletionLocation = computed(() => ({ const emailCompletionLocation = computed(() => ({
name: 'complete-email', name: "complete-email",
query: { query: {
redirect: route.name === 'complete-email' ? '/' : route.fullPath, redirect: route.name === "complete-email" ? "/" : route.fullPath,
}, },
})) }));
// 無障礙狀態 (可持久化儲存於 localStorage) // 無障礙狀態 (可持久化儲存於 localStorage)
const isLargeText = ref(localStorage.getItem('mode-large-text') === 'true') const isLargeText = ref(localStorage.getItem("mode-large-text") === "true");
watch(isLargeText, (newVal) => { watch(isLargeText, (newVal) => {
localStorage.setItem('mode-large-text', String(newVal)) localStorage.setItem("mode-large-text", String(newVal));
}) });
// 同步底部導航狀態與當前路由 // 同步底部導航狀態與當前路由
watch( watch(
() => route.path, () => route.path,
(path) => { (path) => {
if (path === '/') activeTab.value = 'home' if (path === "/") activeTab.value = "home";
else if (path.startsWith('/self-assessment')) activeTab.value = 'self-assessment' else if (path.startsWith("/self-assessment"))
else if (path.startsWith('/appointment')) activeTab.value = 'appointment' activeTab.value = "self-assessment";
else if (path.startsWith('/checkin')) activeTab.value = 'checkin' else if (path.startsWith("/appointment")) activeTab.value = "appointment";
else if (path.startsWith("/checkin")) activeTab.value = "checkin";
}, },
{ immediate: true } { immediate: true },
) );
function getBottomNavIconColor(value: string) { function getBottomNavIconColor(value: string) {
return activeTab.value === value ? 'secondary' : 'secondary-darken-1' return activeTab.value === value ? "secondary" : "secondary-darken-1";
} }
async function handleLogout() { async function handleLogout() {
await store.logout() await store.logout();
courseStore.resetGroup() courseStore.resetGroup();
await router.push('/login') await router.push("/login");
drawer.value = false drawer.value = false;
} }
</script> </script>
+164 -117
View File
@@ -11,12 +11,20 @@
<v-card-title> <v-card-title>
{{ store.userProfile.phone }} {{ store.userProfile.phone }}
</v-card-title> </v-card-title>
<v-card-subtitle> 資料更新於 {{ todayTimeString }} </v-card-subtitle> <v-card-subtitle>
資料更新於 {{ todayTimeString }}
</v-card-subtitle>
</v-col> </v-col>
<v-col v-if="courseStore.groupMissing" class="d-flex justify-center" cols="4"> <v-col
v-if="courseStore.groupMissing"
class="d-flex justify-center"
cols="4"
>
<!-- 體適能分組徽章 --> <!-- 體適能分組徽章 -->
<div class="score-disk d-flex flex-column justify-center align-center text-primary"> <div
class="score-disk d-flex flex-column justify-center align-center text-primary"
>
<span class="group-label">未完成體適能測試</span> <span class="group-label">未完成體適能測試</span>
</div> </div>
</v-col> </v-col>
@@ -24,12 +32,22 @@
<v-card-subtitle v-if="courseStore.groupError" class="text-error"> <v-card-subtitle v-if="courseStore.groupError" class="text-error">
{{ courseStore.groupError }} {{ courseStore.groupError }}
<v-btn class="ml-2" color="error" size="small" variant="text" @click="loadFitnessGroup"> <v-btn
class="ml-2"
color="error"
size="small"
variant="text"
@click="loadFitnessGroup"
>
重試 重試
</v-btn> </v-btn>
</v-card-subtitle> </v-card-subtitle>
<v-row v-if="RADAR_ENABLED && physicalFitnessRadarData" class="mt-4" no-gutters> <v-row
v-if="RADAR_ENABLED && physicalFitnessRadarData"
class="mt-4"
no-gutters
>
<v-col <v-col
v-for="summary in radarSummaries" v-for="summary in radarSummaries"
:key="summary.label" :key="summary.label"
@@ -183,10 +201,17 @@
</v-row> </v-row>
</v-card-text> </v-card-text>
<div class="d-flex align-center mt-4 px-4"> <div class="d-flex align-center mt-4 px-4">
<v-icon class="mr-3" color="primary" icon="mdi-lock-outline" size="28" /> <v-icon
class="mr-3"
color="primary"
icon="mdi-lock-outline"
size="28"
/>
<div> <div>
<div class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div> <div class="font-weight-bold text-grey-darken-4">
登入後可使用完整功能
</div>
</div> </div>
</div> </div>
</v-card> </v-card>
@@ -222,7 +247,12 @@
> >
{{ generateResult }} {{ generateResult }}
</v-alert> </v-alert>
<v-alert v-if="generateError" class="mb-0" type="error" variant="tonal"> <v-alert
v-if="generateError"
class="mb-0"
type="error"
variant="tonal"
>
{{ generateError }} {{ generateError }}
</v-alert> </v-alert>
</v-card-text> </v-card-text>
@@ -235,7 +265,7 @@
variant="text" variant="text"
@click="closeGenerateDialog" @click="closeGenerateDialog"
> >
{{ generateResult || generateError ? '關閉' : '取消' }} {{ generateResult || generateError ? "關閉" : "取消" }}
</v-btn> </v-btn>
<v-btn <v-btn
v-if="!generateResult && !generateError" v-if="!generateResult && !generateError"
@@ -259,7 +289,11 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-dialog v-if="APPOINTMENT_ENABLED" v-model="missingGroupDialog" max-width="400"> <v-dialog
v-if="APPOINTMENT_ENABLED"
v-model="missingGroupDialog"
max-width="400"
>
<v-card class="rounded-xl overflow-hidden text-center"> <v-card class="rounded-xl overflow-hidden text-center">
<v-card-title class="bg-primary text-white font-weight-bold py-3"> <v-card-title class="bg-primary text-white font-weight-bold py-3">
尚未取得體適能分組 尚未取得體適能分組
@@ -305,92 +339,95 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from "vue";
import { useRouter } from 'vue-router' import { useRouter } from "vue-router";
import type { PhysicalFitnessItem } from '@/api/types' import type { PhysicalFitnessItem } from "@/api/types";
import { fetchPhysicalFitness, generatePhysicalFitness } from '@/api/physical-fitness' import {
import BrandAvatar from '@/components/BrandAvatar.vue' fetchPhysicalFitness,
import FitnessGroupCardItem from '@/components/FitnessGroupCardItem.vue' generatePhysicalFitness,
import HealthRadarChart from '@/components/HealthRadarChart.vue' } from "@/api/physical-fitness";
import IntroductionCarousel from '@/components/IntroductionCarousel.vue' import BrandAvatar from "@/components/BrandAvatar.vue";
import Layout from '@/components/layout/DefaultLayout.vue' import FitnessGroupCardItem from "@/components/FitnessGroupCardItem.vue";
import ModuleRouterCard from '@/components/ModuleRouterCard.vue' import HealthRadarChart from "@/components/HealthRadarChart.vue";
import PlatformIntroduction from '@/components/PlatformIntroduction.vue' import IntroductionCarousel from "@/components/IntroductionCarousel.vue";
import PhysicalFitnessSummary from '@/components/PhysicalFitnessSummary.vue' import Layout from "@/components/layout/DefaultLayout.vue";
import ModuleRouterCard from "@/components/ModuleRouterCard.vue";
import PlatformIntroduction from "@/components/PlatformIntroduction.vue";
import PhysicalFitnessSummary from "@/components/PhysicalFitnessSummary.vue";
import { import {
APPOINTMENT_ENABLED, APPOINTMENT_ENABLED,
BODY_COMPOSITION_ENABLED, BODY_COMPOSITION_ENABLED,
CHECKIN_ENABLED, CHECKIN_ENABLED,
RADAR_ENABLED, RADAR_ENABLED,
} from '@/config/features' } from "@/config/features";
import { useAppStore } from '@/stores/app' import { useAppStore } from "@/stores/app";
import { useCourseStore } from '@/stores/course' import { useCourseStore } from "@/stores/course";
import { getApiErrorMessage } from '@/utils/api-error' import { getApiErrorMessage } from "@/utils/api-error";
import { calculatePhysicalFitnessRadar } from '@/utils/physical-fitness-radar' import { calculatePhysicalFitnessRadar } from "@/utils/physical-fitness-radar";
const store = useAppStore() const store = useAppStore();
const courseStore = useCourseStore() const courseStore = useCourseStore();
const router = useRouter() const router = useRouter();
const missingGroupDialog = ref(false) const missingGroupDialog = ref(false);
const generateDialog = ref(false) const generateDialog = ref(false);
const generatePhysicalFitnessLoading = ref(false) const generatePhysicalFitnessLoading = ref(false);
const generateResult = ref('') const generateResult = ref("");
const generateError = ref('') const generateError = ref("");
const physicalFitnessItems = ref<PhysicalFitnessItem[]>([]) const physicalFitnessItems = ref<PhysicalFitnessItem[]>([]);
const physicalFitnessReceivedAt = ref<string | null>(null) const physicalFitnessReceivedAt = ref<string | null>(null);
const physicalFitnessLoading = ref(false) const physicalFitnessLoading = ref(false);
const physicalFitnessError = ref('') const physicalFitnessError = ref("");
const physicalFitnessRadarData = computed(() => const physicalFitnessRadarData = computed(() =>
calculatePhysicalFitnessRadar(physicalFitnessItems.value) calculatePhysicalFitnessRadar(physicalFitnessItems.value),
) );
const radarSummaries = computed(() => { const radarSummaries = computed(() => {
if (!physicalFitnessRadarData.value) { if (!physicalFitnessRadarData.value) {
return [] return [];
} }
return [ return [
{ {
label: '上肢', label: "上肢",
tip: '上肢肌力', tip: "上肢肌力",
score: physicalFitnessRadarData.value.upperBodyStrength, score: physicalFitnessRadarData.value.upperBodyStrength,
}, },
{ {
label: '下肢', label: "下肢",
tip: '下肢肌耐力', tip: "下肢肌耐力",
score: physicalFitnessRadarData.value.lowerBodyStrengthAndEndurance, score: physicalFitnessRadarData.value.lowerBodyStrengthAndEndurance,
}, },
{ {
label: '心肺', label: "心肺",
tip: '心肺耐力', tip: "心肺耐力",
score: physicalFitnessRadarData.value.cardio, score: physicalFitnessRadarData.value.cardio,
}, },
{ {
label: '敏捷', label: "敏捷",
tip: '平衡、敏捷與行動', tip: "平衡、敏捷與行動",
score: physicalFitnessRadarData.value.balanceAgilityAndMobility, score: physicalFitnessRadarData.value.balanceAgilityAndMobility,
}, },
{ {
label: '柔軟', label: "柔軟",
tip: '柔軟度', tip: "柔軟度",
score: physicalFitnessRadarData.value.flexibility, score: physicalFitnessRadarData.value.flexibility,
}, },
] ];
}) });
const MISSING_GROUP_DIALOG_KEY = 'missing-fitness-group-dialog-shown' const MISSING_GROUP_DIALOG_KEY = "missing-fitness-group-dialog-shown";
function hasShownMissingGroupDialog() { function hasShownMissingGroupDialog() {
try { try {
return sessionStorage.getItem(MISSING_GROUP_DIALOG_KEY) === 'true' return sessionStorage.getItem(MISSING_GROUP_DIALOG_KEY) === "true";
} catch { } catch {
return false return false;
} }
} }
function markMissingGroupDialogShown() { function markMissingGroupDialogShown() {
try { try {
sessionStorage.setItem(MISSING_GROUP_DIALOG_KEY, 'true') sessionStorage.setItem(MISSING_GROUP_DIALOG_KEY, "true");
} catch { } catch {
// 部分瀏覽器可能封鎖 sessionStorage,仍允許本次彈窗正常使用。 // 部分瀏覽器可能封鎖 sessionStorage,仍允許本次彈窗正常使用。
} }
@@ -398,144 +435,154 @@ function markMissingGroupDialogShown() {
function clearMissingGroupDialogMarker() { function clearMissingGroupDialogMarker() {
try { try {
sessionStorage.removeItem(MISSING_GROUP_DIALOG_KEY) sessionStorage.removeItem(MISSING_GROUP_DIALOG_KEY);
} catch { } catch {
// 不影響主要流程。 // 不影響主要流程。
} }
} }
function openMissingGroupDialog() { function openMissingGroupDialog() {
if (store.isLoggedIn && courseStore.groupMissing && !hasShownMissingGroupDialog()) { if (
markMissingGroupDialogShown() store.isLoggedIn &&
missingGroupDialog.value = true courseStore.groupMissing &&
!hasShownMissingGroupDialog()
) {
markMissingGroupDialogShown();
missingGroupDialog.value = true;
} }
} }
async function goToAppointment() { async function goToAppointment() {
missingGroupDialog.value = false missingGroupDialog.value = false;
await router.push({ name: 'appointment' }) await router.push({ name: "appointment" });
} }
async function loadFitnessGroup() { async function loadFitnessGroup() {
if (!store.isLoggedIn) { if (!store.isLoggedIn) {
missingGroupDialog.value = false missingGroupDialog.value = false;
clearMissingGroupDialogMarker() clearMissingGroupDialogMarker();
courseStore.resetGroup() courseStore.resetGroup();
return return;
} }
try { try {
await courseStore.loadGroup() await courseStore.loadGroup();
clearMissingGroupDialogMarker() clearMissingGroupDialogMarker();
} catch { } catch {
// 錯誤狀態由 course store 提供給畫面顯示與重試。 // 錯誤狀態由 course store 提供給畫面顯示與重試。
openMissingGroupDialog() openMissingGroupDialog();
} }
} }
function getRequiredUserID(): string | null { function getRequiredUserID(): string | null {
const userID = store.userID.trim() const userID = store.userID.trim();
if (userID) { if (userID) {
return userID return userID;
} }
return null return null;
} }
async function loadPhysicalFitness() { async function loadPhysicalFitness() {
if (!store.isLoggedIn) { if (!store.isLoggedIn) {
physicalFitnessItems.value = [] physicalFitnessItems.value = [];
physicalFitnessReceivedAt.value = null physicalFitnessReceivedAt.value = null;
physicalFitnessError.value = '' physicalFitnessError.value = "";
return return;
} }
const userID = getRequiredUserID() const userID = getRequiredUserID();
if (!userID) { if (!userID) {
return return;
} }
physicalFitnessLoading.value = true physicalFitnessLoading.value = true;
physicalFitnessError.value = '' physicalFitnessError.value = "";
try { try {
const physicalFitnessResponse = await fetchPhysicalFitness(userID) const physicalFitnessResponse = await fetchPhysicalFitness(userID);
physicalFitnessItems.value = physicalFitnessResponse.results physicalFitnessItems.value = physicalFitnessResponse.results;
physicalFitnessReceivedAt.value = physicalFitnessResponse.ReceivedAt physicalFitnessReceivedAt.value = physicalFitnessResponse.ReceivedAt;
updateTodayTimeString() updateTodayTimeString();
} catch (error) { } catch (error) {
physicalFitnessError.value = getApiErrorMessage(error, '體適能測驗資料載入失敗,請稍後再試。') physicalFitnessError.value = getApiErrorMessage(
error,
"體適能測驗資料載入失敗,請稍後再試。",
);
} finally { } finally {
physicalFitnessLoading.value = false physicalFitnessLoading.value = false;
} }
} }
function openGenerateDialog() { function openGenerateDialog() {
if (!getRequiredUserID()) { if (!getRequiredUserID()) {
return return;
} }
generateResult.value = '' generateResult.value = "";
generateError.value = '' generateError.value = "";
generateDialog.value = true generateDialog.value = true;
} }
function closeGenerateDialog() { function closeGenerateDialog() {
if (generatePhysicalFitnessLoading.value) { if (generatePhysicalFitnessLoading.value) {
return return;
} }
generateDialog.value = false generateDialog.value = false;
} }
async function handleGeneratePhysicalFitness() { async function handleGeneratePhysicalFitness() {
const userID = getRequiredUserID() const userID = getRequiredUserID();
if (!userID) { if (!userID) {
return return;
} }
generatePhysicalFitnessLoading.value = true generatePhysicalFitnessLoading.value = true;
generateResult.value = '' generateResult.value = "";
generateError.value = '' generateError.value = "";
try { try {
const response = await generatePhysicalFitness(userID) const response = await generatePhysicalFitness(userID);
generateResult.value = response.message generateResult.value = response.message;
updateTodayTimeString() updateTodayTimeString();
if (response.message === '評級完成') { if (response.message === "評級完成") {
await courseStore.loadGroup(true) await courseStore.loadGroup(true);
} }
await loadPhysicalFitness() await loadPhysicalFitness();
} catch (error) { } catch (error) {
generateError.value = getApiErrorMessage(error, '體適能評級重新計算失敗,請稍後再試。') generateError.value = getApiErrorMessage(
error,
"體適能評級重新計算失敗,請稍後再試。",
);
} finally { } finally {
generatePhysicalFitnessLoading.value = false generatePhysicalFitnessLoading.value = false;
} }
} }
watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true }) watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true });
watch([() => store.isLoggedIn, () => store.userID], loadPhysicalFitness, { watch([() => store.isLoggedIn, () => store.userID], loadPhysicalFitness, {
immediate: true, immediate: true,
}) });
function formatCurrentDateTime() { function formatCurrentDateTime() {
const now = new Date() const now = new Date();
const date = [ const date = [
String(now.getMonth() + 1).padStart(2, '0'), String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, '0'), String(now.getDate()).padStart(2, "0"),
].join('/') ].join("/");
const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
return `${date} ${time}` return `${date} ${time}`;
} }
const todayTimeString = ref(formatCurrentDateTime()) const todayTimeString = ref(formatCurrentDateTime());
function updateTodayTimeString() { function updateTodayTimeString() {
todayTimeString.value = formatCurrentDateTime() todayTimeString.value = formatCurrentDateTime();
} }
</script> </script>
+10
View File
@@ -0,0 +1,10 @@
{
"name": "sports-empower-hub",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sports-empower-hub"
}
}
}