fix: 文字
This commit is contained in:
@@ -1,26 +1,20 @@
|
|||||||
<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-alert v-if="errorMessage" class="mb-4" density="compact" type="error" variant="tonal">
|
||||||
v-if="errorMessage"
|
|
||||||
class="mb-4"
|
|
||||||
density="compact"
|
|
||||||
type="error"
|
|
||||||
variant="tonal"
|
|
||||||
>
|
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
@@ -33,7 +27,7 @@ const emit = defineEmits<{
|
|||||||
label="手機號碼"
|
label="手機號碼"
|
||||||
maxlength="14"
|
maxlength="14"
|
||||||
placeholder="例如:0912345678"
|
placeholder="例如:0912345678"
|
||||||
prepend-inner-icon="mdi-cellphone-android"
|
prepend-inner-icon="mdi-phone"
|
||||||
required
|
required
|
||||||
type="tel"
|
type="tel"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -47,7 +41,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,9 +6,7 @@
|
|||||||
<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 class="font-weight-bold flex-grow-1"> 運動玩轉健康力 </v-app-bar-title>
|
||||||
運動玩轉健康力
|
|
||||||
</v-app-bar-title>
|
|
||||||
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
@@ -21,10 +19,7 @@
|
|||||||
variant="text"
|
variant="text"
|
||||||
@click="isLargeText = !isLargeText"
|
@click="isLargeText = !isLargeText"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon :color="isLargeText ? 'accent' : 'white'" icon="mdi-format-size" />
|
||||||
:color="isLargeText ? 'accent' : 'white'"
|
|
||||||
icon="mdi-format-size"
|
|
||||||
/>
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -66,9 +61,7 @@
|
|||||||
</v-avatar>
|
</v-avatar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-list-item-subtitle class="text-white-50"
|
<v-list-item-subtitle class="text-white-50">未登入訪客</v-list-item-subtitle>
|
||||||
>未登入訪客</v-list-item-subtitle
|
|
||||||
>
|
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
@@ -81,12 +74,7 @@
|
|||||||
<v-list-item-title>大字體模式</v-list-item-title>
|
<v-list-item-title>大字體模式</v-list-item-title>
|
||||||
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<v-switch
|
<v-switch v-model="isLargeText" color="secondary" density="compact" hide-details />
|
||||||
v-model="isLargeText"
|
|
||||||
color="secondary"
|
|
||||||
density="compact"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
@@ -203,36 +191,26 @@
|
|||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<!-- 主要內容區 -->
|
<!-- 主要內容區 -->
|
||||||
<v-main
|
<v-main class="bg-white min-vh-100" :class="{ 'pb-16': BOTTOM_NAVIGATION_ENABLED }">
|
||||||
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.isLoggedIn && store.requiresEmailCompletion && route.name !== 'complete-email'
|
||||||
store.requiresEmailCompletion &&
|
|
||||||
route.name !== 'complete-email'
|
|
||||||
"
|
"
|
||||||
class="mb-4"
|
class="mb-2"
|
||||||
color="warning"
|
color="warning"
|
||||||
icon="mdi-email-alert-outline"
|
icon="mdi-email-alert-outline"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
>
|
>
|
||||||
<div class="d-flex flex-wrap align-center justify-space-between ga-2">
|
<div class="d-flex flex-wrap align-center justify-space-between ga-2">
|
||||||
<span>
|
<span class="text-title-small">
|
||||||
{{
|
{{
|
||||||
EMAIL_COMPLETION_REQUIRED
|
EMAIL_COMPLETION_REQUIRED
|
||||||
? "請先補登 E-Mail,才能繼續使用平台。"
|
? '請先補登 E-Mail,才能繼續使用平台。'
|
||||||
: "尚未設定 E-Mail,前往補登以接收重要通知。"
|
: '尚未設定 E-Mail,前往補登以接收重要通知。'
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<v-btn
|
<v-btn color="warning" size="small" :to="emailCompletionLocation" variant="flat">
|
||||||
color="warning"
|
|
||||||
size="small"
|
|
||||||
:to="emailCompletionLocation"
|
|
||||||
variant="flat"
|
|
||||||
>
|
|
||||||
前往補登
|
前往補登
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,9 +230,7 @@
|
|||||||
tag="div"
|
tag="div"
|
||||||
>
|
>
|
||||||
<v-btn to="/" value="home">
|
<v-btn to="/" value="home">
|
||||||
<v-icon :color="getBottomNavIconColor('home')"
|
<v-icon :color="getBottomNavIconColor('home')">mdi-view-dashboard</v-icon>
|
||||||
>mdi-view-dashboard</v-icon
|
|
||||||
>
|
|
||||||
<span class="bottom-nav-text">首頁</span>
|
<span class="bottom-nav-text">首頁</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
@@ -270,25 +246,13 @@
|
|||||||
<span class="bottom-nav-text">自評</span>
|
<span class="bottom-nav-text">自評</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn
|
<v-btn v-if="CHECKIN_ENABLED && store.isLoggedIn" to="/checkin" value="checkin">
|
||||||
v-if="CHECKIN_ENABLED && store.isLoggedIn"
|
<v-icon :color="getBottomNavIconColor('checkin')">mdi-qrcode-scan</v-icon>
|
||||||
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-btn v-if="APPOINTMENT_ENABLED && store.isLoggedIn" to="/appointment" value="appointment">
|
||||||
v-if="APPOINTMENT_ENABLED && store.isLoggedIn"
|
<v-icon :color="getBottomNavIconColor('appointment')">mdi-calendar-check</v-icon>
|
||||||
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>
|
||||||
@@ -296,9 +260,7 @@
|
|||||||
<!-- 隱私授權對話框 -->
|
<!-- 隱私授權對話框 -->
|
||||||
<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
|
<v-card-title class="bg-primary text-white font-weight-bold d-flex align-center">
|
||||||
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>
|
||||||
@@ -331,8 +293,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,
|
||||||
@@ -340,54 +302,53 @@ 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"))
|
else if (path.startsWith('/self-assessment')) activeTab.value = 'self-assessment'
|
||||||
activeTab.value = "self-assessment";
|
else if (path.startsWith('/appointment')) activeTab.value = 'appointment'
|
||||||
else if (path.startsWith("/appointment")) activeTab.value = "appointment";
|
else if (path.startsWith('/checkin')) activeTab.value = 'checkin'
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
@@ -11,20 +11,12 @@
|
|||||||
<v-card-title>
|
<v-card-title>
|
||||||
{{ store.userProfile.phone }}
|
{{ store.userProfile.phone }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-subtitle>
|
<v-card-subtitle> 資料更新於 {{ todayTimeString }} </v-card-subtitle>
|
||||||
資料更新於 {{ todayTimeString }}
|
|
||||||
</v-card-subtitle>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col
|
<v-col v-if="courseStore.groupMissing" class="d-flex justify-center" cols="4">
|
||||||
v-if="courseStore.groupMissing"
|
|
||||||
class="d-flex justify-center"
|
|
||||||
cols="4"
|
|
||||||
>
|
|
||||||
<!-- 體適能分組徽章 -->
|
<!-- 體適能分組徽章 -->
|
||||||
<div
|
<div class="score-disk d-flex flex-column justify-center align-center text-primary">
|
||||||
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>
|
||||||
@@ -32,22 +24,12 @@
|
|||||||
|
|
||||||
<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
|
<v-btn class="ml-2" color="error" size="small" variant="text" @click="loadFitnessGroup">
|
||||||
class="ml-2"
|
|
||||||
color="error"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
@click="loadFitnessGroup"
|
|
||||||
>
|
|
||||||
重試
|
重試
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-subtitle>
|
</v-card-subtitle>
|
||||||
|
|
||||||
<v-row
|
<v-row v-if="RADAR_ENABLED && physicalFitnessRadarData" class="mt-4" no-gutters>
|
||||||
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"
|
||||||
@@ -195,23 +177,16 @@
|
|||||||
to="/register"
|
to="/register"
|
||||||
variant="text"
|
variant="text"
|
||||||
>
|
>
|
||||||
建立帳號
|
既有帳號建立密碼
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</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
|
<v-icon class="mr-3" color="primary" icon="mdi-lock-outline" size="28" />
|
||||||
class="mr-3"
|
|
||||||
color="primary"
|
|
||||||
icon="mdi-lock-outline"
|
|
||||||
size="28"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="font-weight-bold text-grey-darken-4">
|
<div class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div>
|
||||||
登入後可使用完整功能
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -247,12 +222,7 @@
|
|||||||
>
|
>
|
||||||
{{ generateResult }}
|
{{ generateResult }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
<v-alert
|
<v-alert v-if="generateError" class="mb-0" type="error" variant="tonal">
|
||||||
v-if="generateError"
|
|
||||||
class="mb-0"
|
|
||||||
type="error"
|
|
||||||
variant="tonal"
|
|
||||||
>
|
|
||||||
{{ generateError }}
|
{{ generateError }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -265,7 +235,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"
|
||||||
@@ -289,11 +259,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<v-dialog
|
<v-dialog v-if="APPOINTMENT_ENABLED" v-model="missingGroupDialog" max-width="400">
|
||||||
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">
|
||||||
尚未取得體適能分組
|
尚未取得體適能分組
|
||||||
@@ -339,95 +305,92 @@
|
|||||||
</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 {
|
import { fetchPhysicalFitness, generatePhysicalFitness } from '@/api/physical-fitness'
|
||||||
fetchPhysicalFitness,
|
import BrandAvatar from '@/components/BrandAvatar.vue'
|
||||||
generatePhysicalFitness,
|
import FitnessGroupCardItem from '@/components/FitnessGroupCardItem.vue'
|
||||||
} from "@/api/physical-fitness";
|
import HealthRadarChart from '@/components/HealthRadarChart.vue'
|
||||||
import BrandAvatar from "@/components/BrandAvatar.vue";
|
import IntroductionCarousel from '@/components/IntroductionCarousel.vue'
|
||||||
import FitnessGroupCardItem from "@/components/FitnessGroupCardItem.vue";
|
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||||
import HealthRadarChart from "@/components/HealthRadarChart.vue";
|
import ModuleRouterCard from '@/components/ModuleRouterCard.vue'
|
||||||
import IntroductionCarousel from "@/components/IntroductionCarousel.vue";
|
import PlatformIntroduction from '@/components/PlatformIntroduction.vue'
|
||||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
import PhysicalFitnessSummary from '@/components/PhysicalFitnessSummary.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,仍允許本次彈窗正常使用。
|
||||||
}
|
}
|
||||||
@@ -435,154 +398,144 @@ 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 (
|
if (store.isLoggedIn && courseStore.groupMissing && !hasShownMissingGroupDialog()) {
|
||||||
store.isLoggedIn &&
|
markMissingGroupDialogShown()
|
||||||
courseStore.groupMissing &&
|
missingGroupDialog.value = true
|
||||||
!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(
|
physicalFitnessError.value = getApiErrorMessage(error, '體適能測驗資料載入失敗,請稍後再試。')
|
||||||
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(
|
generateError.value = getApiErrorMessage(error, '體適能評級重新計算失敗,請稍後再試。')
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<BrandAvatar />
|
<BrandAvatar />
|
||||||
<v-card-title class="text-primary font-weight-bold">
|
<v-card-title class="text-primary font-weight-bold">
|
||||||
建立帳號
|
建立密碼
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|||||||
Reference in New Issue
Block a user