This commit is contained in:
skytek_xinliang
2026-09-21 16:02:30 +08:00
parent 92ef1251bf
commit b8997975a5
4 changed files with 325 additions and 193 deletions
@@ -47,23 +47,58 @@
stroke-width="0.5"
/>
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="middle" x="60" y="8">
<text
fill="#1D7063"
font-size="4"
font-weight="bold"
text-anchor="middle"
x="60"
y="8"
>
心肺 ({{ radarData.cardio }})
</text>
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="start" x="110" y="44">
<text
fill="#1D7063"
font-size="4"
font-weight="bold"
text-anchor="start"
x="110"
y="44"
>
上肢 ({{ radarData.upperBodyStrength }})
</text>
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="start" x="94" y="104">
<text
fill="#1D7063"
font-size="4"
font-weight="bold"
text-anchor="start"
x="94"
y="104"
>
下肢 ({{ radarData.lowerBodyStrengthAndEndurance }})
</text>
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="end" x="26" y="104">
<text
fill="#1D7063"
font-size="4"
font-weight="bold"
text-anchor="end"
x="26"
y="104"
>
柔軟 ({{ radarData.flexibility }})
</text>
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="end" x="10" y="44">
<text
fill="#1D7063"
font-size="4"
font-weight="bold"
text-anchor="end"
x="10"
y="44"
>
敏捷 ({{ radarData.balanceAgilityAndMobility }})
</text>
</svg>
@@ -83,36 +118,36 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { PhysicalFitnessRadarData } from '@/utils/physical-fitness-radar'
import { computed } from "vue";
import type { PhysicalFitnessRadarData } from "@/utils/physical-fitness-radar";
const { radarData } = defineProps<{
radarData: PhysicalFitnessRadarData
}>()
radarData: PhysicalFitnessRadarData;
}>();
const centerX = 60
const centerY = 60
const radius = 45
const centerX = 60;
const centerY = 60;
const radius = 45;
function getAxisEndPoint(index: number, pointRadius = radius) {
const angle = -Math.PI / 2 + (index * 2 * Math.PI) / 5
const angle = -Math.PI / 2 + (index * 2 * Math.PI) / 5;
return {
x: centerX + pointRadius * Math.cos(angle),
y: centerY + pointRadius * Math.sin(angle),
}
};
}
function getPolygonPoints(percent: number) {
const pointRadius = (percent / 100) * radius
const points = []
const pointRadius = (percent / 100) * radius;
const points = [];
for (let index = 0; index < 5; index++) {
const point = getAxisEndPoint(index, pointRadius)
points.push(`${point.x},${point.y}`)
const point = getAxisEndPoint(index, pointRadius);
points.push(`${point.x},${point.y}`);
}
return points.join(' ')
return points.join(" ");
}
const userDataCoordArray = computed(() => {
@@ -122,17 +157,19 @@ const userDataCoordArray = computed(() => {
radarData.lowerBodyStrengthAndEndurance,
radarData.flexibility,
radarData.balanceAgilityAndMobility,
]
];
return values.map((value, index) => {
const pointRadius = (Math.max(5, Math.min(100, value)) / 100) * radius
return getAxisEndPoint(index, pointRadius)
})
})
const pointRadius = (Math.max(5, Math.min(100, value)) / 100) * radius;
return getAxisEndPoint(index, pointRadius);
});
});
const userDataPoints = computed(() => {
return userDataCoordArray.value.map((point) => `${point.x},${point.y}`).join(' ')
})
return userDataCoordArray.value
.map((point) => `${point.x},${point.y}`)
.join(" ");
});
</script>
<style scoped>
@@ -6,7 +6,9 @@
<v-app-bar-nav-icon variant="text" @click="drawer = !drawer" />
</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>
<div class="d-flex align-center">
@@ -19,7 +21,10 @@
variant="text"
@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>
</div>
</template>
@@ -37,7 +42,7 @@
<template #prepend>
<v-avatar color="accent" size="48">
<span class="text-white font-weight-bold text-h6">
{{ (store.userProfile.name || '姓名').substring(0, 1) }}
{{ (store.userProfile.name || "姓名").substring(0, 1) }}
</span>
</v-avatar>
</template>
@@ -58,7 +63,9 @@
</v-avatar>
</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-divider />
@@ -71,7 +78,12 @@
<v-list-item-title>大字體模式</v-list-item-title>
<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>
</v-list-item>
@@ -171,7 +183,10 @@
</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">
<slot name="main" />
</v-container>
@@ -188,11 +203,17 @@
tag="div"
>
<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>
</v-btn>
<v-btn v-if="SELF_ASSESSMENT_ENABLED" to="/self-assessment" value="self-assessment">
<v-btn
v-if="SELF_ASSESSMENT_ENABLED"
to="/self-assessment"
value="self-assessment"
>
<v-icon :color="getBottomNavIconColor('self-assessment')">
mdi-clipboard-text-search
</v-icon>
@@ -200,13 +221,25 @@
<span class="bottom-nav-text">自評</span>
</v-btn>
<v-btn v-if="CHECKIN_ENABLED && store.isLoggedIn" to="/checkin" value="checkin">
<v-icon :color="getBottomNavIconColor('checkin')">mdi-qrcode-scan</v-icon>
<v-btn
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>
</v-btn>
<v-btn v-if="APPOINTMENT_ENABLED && store.isLoggedIn" to="/appointment" value="appointment">
<v-icon :color="getBottomNavIconColor('appointment')">mdi-calendar-check</v-icon>
<v-btn
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>
</v-btn>
</v-bottom-navigation>
@@ -214,7 +247,9 @@
<!-- 隱私授權對話框 -->
<v-dialog v-model="showPrivacy" max-width="500">
<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-card-title>
@@ -231,7 +266,7 @@
</ul>
<p class="text-caption text-grey">
授權狀態:{{ store.userProfile.consent ? '已同意授權' : '未授權' }}
授權狀態:{{ store.userProfile.consent ? "已同意授權" : "未授權" }}
</p>
</v-card-text>
@@ -251,57 +286,58 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useDisplayUserName } from '@/composables/useDisplayUserName'
import { ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useDisplayUserName } from "@/composables/useDisplayUserName";
import {
APPOINTMENT_ENABLED,
BODY_COMPOSITION_ENABLED,
BOTTOM_NAVIGATION_ENABLED,
CHECKIN_ENABLED,
SELF_ASSESSMENT_ENABLED,
} from '@/config/features'
import { useAppStore } from '@/stores/app'
import { useCourseStore } from '@/stores/course'
} from "@/config/features";
import { useAppStore } from "@/stores/app";
import { useCourseStore } from "@/stores/course";
const store = useAppStore()
const courseStore = useCourseStore()
const router = useRouter()
const route = useRoute()
const displayUserName = useDisplayUserName(store.userProfile)
const store = useAppStore();
const courseStore = useCourseStore();
const router = useRouter();
const route = useRoute();
const displayUserName = useDisplayUserName(store.userProfile);
const drawer = ref(false)
const activeTab = ref('home')
const showPrivacy = ref(false)
const drawer = ref(false);
const activeTab = ref("home");
const showPrivacy = ref(false);
// 無障礙狀態 (可持久化儲存於 localStorage)
const isLargeText = ref(localStorage.getItem('mode-large-text') === 'true')
const isLargeText = ref(localStorage.getItem("mode-large-text") === "true");
watch(isLargeText, (newVal) => {
localStorage.setItem('mode-large-text', String(newVal))
})
localStorage.setItem("mode-large-text", String(newVal));
});
// 同步底部導航狀態與當前路由
watch(
() => route.path,
(path) => {
if (path === '/') activeTab.value = 'home'
else if (path.startsWith('/self-assessment')) activeTab.value = 'self-assessment'
else if (path.startsWith('/appointment')) activeTab.value = 'appointment'
else if (path.startsWith('/checkin')) activeTab.value = 'checkin'
if (path === "/") activeTab.value = "home";
else if (path.startsWith("/self-assessment"))
activeTab.value = "self-assessment";
else if (path.startsWith("/appointment")) activeTab.value = "appointment";
else if (path.startsWith("/checkin")) activeTab.value = "checkin";
},
{ immediate: true }
)
{ immediate: true },
);
function getBottomNavIconColor(value: string) {
return activeTab.value === value ? 'secondary' : 'secondary-darken-1'
return activeTab.value === value ? "secondary" : "secondary-darken-1";
}
async function handleLogout() {
await store.logout()
courseStore.resetGroup()
await router.push('/login')
drawer.value = false
await store.logout();
courseStore.resetGroup();
await router.push("/login");
drawer.value = false;
}
</script>
+7 -7
View File
@@ -1,7 +1,7 @@
export const APPOINTMENT_ENABLED = false
export const BODY_COMPOSITION_ENABLED = true
export const BOTTOM_NAVIGATION_ENABLED = false
export const CHECKIN_ENABLED = false
export const EXERCISE_DURATION_RECOMMENDATION_ENABLED = false
export const RADAR_ENABLED = true
export const SELF_ASSESSMENT_ENABLED = true
export const APPOINTMENT_ENABLED = false;
export const BODY_COMPOSITION_ENABLED = true;
export const BOTTOM_NAVIGATION_ENABLED = false;
export const CHECKIN_ENABLED = false;
export const EXERCISE_DURATION_RECOMMENDATION_ENABLED = false;
export const RADAR_ENABLED = true;
export const SELF_ASSESSMENT_ENABLED = true;
+177 -118
View File
@@ -11,12 +11,20 @@
<v-card-title>
{{ displayUserName }}
</v-card-title>
<v-card-subtitle> 資料更新於 {{ todayTimeString }} </v-card-subtitle>
<v-card-subtitle>
資料更新於 {{ todayTimeString }}
</v-card-subtitle>
</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>
</div>
</v-col>
@@ -24,12 +32,22 @@
<v-card-subtitle v-if="courseStore.groupError" class="text-error">
{{ 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-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-for="summary in radarSummaries"
:key="summary.label"
@@ -69,7 +87,11 @@
:group-meta="courseStore.groupMeta"
/>
<v-list-item append-icon="mdi-chevron-right" class="bg-primary py-4" to="/course">
<v-list-item
append-icon="mdi-chevron-right"
class="bg-primary py-4"
to="/course"
>
<v-list-item-title class="font-weight-bold">
<v-icon icon="mdi-notebook-check-outline" size="16" />
課程建議
@@ -206,10 +228,17 @@
</v-row>
</v-card-text>
<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 class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div>
<div class="font-weight-bold text-grey-darken-4">
登入後可使用完整功能
</div>
</div>
</div>
</v-card>
@@ -245,7 +274,12 @@
>
{{ generateResult }}
</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 }}
</v-alert>
</v-card-text>
@@ -258,7 +292,7 @@
variant="text"
@click="closeGenerateDialog"
>
{{ generateResult || generateError ? '關閉' : '取消' }}
{{ generateResult || generateError ? "關閉" : "取消" }}
</v-btn>
<v-btn
v-if="!generateResult && !generateError"
@@ -282,7 +316,11 @@
</v-card>
</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-title class="bg-primary text-white font-weight-bold py-3">
尚未取得體適能分組
@@ -328,87 +366,98 @@
</template>
<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 FitnessGroupCardItem from '@/components/FitnessGroupCardItem.vue'
import HealthRadarChart from '@/components/HealthRadarChart.vue'
import IntroductionCarousel from '@/components/IntroductionCarousel.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 { useDisplayUserName } from '@/composables/useDisplayUserName'
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 FitnessGroupCardItem from "@/components/FitnessGroupCardItem.vue";
import HealthRadarChart from "@/components/HealthRadarChart.vue";
import IntroductionCarousel from "@/components/IntroductionCarousel.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 { useDisplayUserName } from "@/composables/useDisplayUserName";
import {
APPOINTMENT_ENABLED,
BODY_COMPOSITION_ENABLED,
CHECKIN_ENABLED,
RADAR_ENABLED,
SELF_ASSESSMENT_ENABLED,
} from '@/config/features'
import { useAppStore } from '@/stores/app'
import { useCourseStore } from '@/stores/course'
import { getApiErrorMessage } from '@/utils/api-error'
import { calculatePhysicalFitnessRadar } from '@/utils/physical-fitness-radar'
} from "@/config/features";
import { useAppStore } from "@/stores/app";
import { useCourseStore } from "@/stores/course";
import { getApiErrorMessage } from "@/utils/api-error";
import { calculatePhysicalFitnessRadar } from "@/utils/physical-fitness-radar";
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 physicalFitnessReceivedAt = ref<string | null>(null)
const physicalFitnessLoading = ref(false)
const physicalFitnessError = ref('')
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 physicalFitnessReceivedAt = ref<string | null>(null);
const physicalFitnessLoading = ref(false);
const physicalFitnessError = ref("");
const displayUserName = useDisplayUserName(store.userProfile)
const displayUserName = useDisplayUserName(store.userProfile);
const physicalFitnessRadarData = computed(() =>
calculatePhysicalFitnessRadar(physicalFitnessItems.value)
)
calculatePhysicalFitnessRadar(physicalFitnessItems.value),
);
const radarSummaries = computed(() => {
if (!physicalFitnessRadarData.value) {
return []
return [];
}
return [
{
label: '上肢',
tip: '上肢肌力',
label: "上肢",
tip: "上肢肌力",
score: physicalFitnessRadarData.value.upperBodyStrength,
},
{
label: '下肢',
tip: '下肢肌耐力',
label: "下肢",
tip: "下肢肌耐力",
score: physicalFitnessRadarData.value.lowerBodyStrengthAndEndurance,
},
{ label: '心肺', tip: '心肺耐力', score: physicalFitnessRadarData.value.cardio },
{
label: '敏捷',
tip: '平衡、敏捷與行動',
label: "心肺",
tip: "心肺耐力",
score: physicalFitnessRadarData.value.cardio,
},
{
label: "敏捷",
tip: "平衡、敏捷與行動",
score: physicalFitnessRadarData.value.balanceAgilityAndMobility,
},
{ label: '柔軟', tip: '柔軟度', score: physicalFitnessRadarData.value.flexibility },
]
})
{
label: "柔軟",
tip: "柔軟度",
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() {
try {
return sessionStorage.getItem(MISSING_GROUP_DIALOG_KEY) === 'true'
return sessionStorage.getItem(MISSING_GROUP_DIALOG_KEY) === "true";
} catch {
return false
return false;
}
}
function markMissingGroupDialogShown() {
try {
sessionStorage.setItem(MISSING_GROUP_DIALOG_KEY, 'true')
sessionStorage.setItem(MISSING_GROUP_DIALOG_KEY, "true");
} catch {
// 部分瀏覽器可能封鎖 sessionStorage,仍允許本次彈窗正常使用。
}
@@ -416,144 +465,154 @@ function markMissingGroupDialogShown() {
function clearMissingGroupDialogMarker() {
try {
sessionStorage.removeItem(MISSING_GROUP_DIALOG_KEY)
sessionStorage.removeItem(MISSING_GROUP_DIALOG_KEY);
} catch {
// 不影響主要流程。
}
}
function openMissingGroupDialog() {
if (store.isLoggedIn && courseStore.groupMissing && !hasShownMissingGroupDialog()) {
markMissingGroupDialogShown()
missingGroupDialog.value = true
if (
store.isLoggedIn &&
courseStore.groupMissing &&
!hasShownMissingGroupDialog()
) {
markMissingGroupDialogShown();
missingGroupDialog.value = true;
}
}
async function goToAppointment() {
missingGroupDialog.value = false
await router.push({ name: 'appointment' })
missingGroupDialog.value = false;
await router.push({ name: "appointment" });
}
async function loadFitnessGroup() {
if (!store.isLoggedIn) {
missingGroupDialog.value = false
clearMissingGroupDialogMarker()
courseStore.resetGroup()
return
missingGroupDialog.value = false;
clearMissingGroupDialogMarker();
courseStore.resetGroup();
return;
}
try {
await courseStore.loadGroup()
clearMissingGroupDialogMarker()
await courseStore.loadGroup();
clearMissingGroupDialogMarker();
} catch {
// 錯誤狀態由 course store 提供給畫面顯示與重試。
openMissingGroupDialog()
openMissingGroupDialog();
}
}
function getRequiredUserID(): string | null {
const userID = store.userID.trim()
const userID = store.userID.trim();
if (userID) {
return userID
return userID;
}
return null
return null;
}
async function loadPhysicalFitness() {
if (!store.isLoggedIn) {
physicalFitnessItems.value = []
physicalFitnessReceivedAt.value = null
physicalFitnessError.value = ''
return
physicalFitnessItems.value = [];
physicalFitnessReceivedAt.value = null;
physicalFitnessError.value = "";
return;
}
const userID = getRequiredUserID()
const userID = getRequiredUserID();
if (!userID) {
return
return;
}
physicalFitnessLoading.value = true
physicalFitnessError.value = ''
physicalFitnessLoading.value = true;
physicalFitnessError.value = "";
try {
const physicalFitnessResponse = await fetchPhysicalFitness(userID)
physicalFitnessItems.value = physicalFitnessResponse.results
physicalFitnessReceivedAt.value = physicalFitnessResponse.ReceivedAt
updateTodayTimeString()
const physicalFitnessResponse = await fetchPhysicalFitness(userID);
physicalFitnessItems.value = physicalFitnessResponse.results;
physicalFitnessReceivedAt.value = physicalFitnessResponse.ReceivedAt;
updateTodayTimeString();
} catch (error) {
physicalFitnessError.value = getApiErrorMessage(error, '體適能測驗資料載入失敗,請稍後再試。')
physicalFitnessError.value = getApiErrorMessage(
error,
"體適能測驗資料載入失敗,請稍後再試。",
);
} finally {
physicalFitnessLoading.value = false
physicalFitnessLoading.value = false;
}
}
function openGenerateDialog() {
if (!getRequiredUserID()) {
return
return;
}
generateResult.value = ''
generateError.value = ''
generateDialog.value = true
generateResult.value = "";
generateError.value = "";
generateDialog.value = true;
}
function closeGenerateDialog() {
if (generatePhysicalFitnessLoading.value) {
return
return;
}
generateDialog.value = false
generateDialog.value = false;
}
async function handleGeneratePhysicalFitness() {
const userID = getRequiredUserID()
const userID = getRequiredUserID();
if (!userID) {
return
return;
}
generatePhysicalFitnessLoading.value = true
generateResult.value = ''
generateError.value = ''
generatePhysicalFitnessLoading.value = true;
generateResult.value = "";
generateError.value = "";
try {
const response = await generatePhysicalFitness(userID)
generateResult.value = response.message
updateTodayTimeString()
const response = await generatePhysicalFitness(userID);
generateResult.value = response.message;
updateTodayTimeString();
if (response.message === '評級完成') {
await courseStore.loadGroup(true)
if (response.message === "評級完成") {
await courseStore.loadGroup(true);
}
await loadPhysicalFitness()
await loadPhysicalFitness();
} catch (error) {
generateError.value = getApiErrorMessage(error, '體適能評級重新計算失敗,請稍後再試。')
generateError.value = getApiErrorMessage(
error,
"體適能評級重新計算失敗,請稍後再試。",
);
} 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, {
immediate: true,
})
});
function formatCurrentDateTime() {
const now = new Date()
const now = new Date();
const date = [
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
].join('/')
const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0"),
].join("/");
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() {
todayTimeString.value = formatCurrentDateTime()
todayTimeString.value = formatCurrentDateTime();
}
</script>