feat: carousel

This commit is contained in:
skytek_xinliang
2026-09-21 14:27:35 +08:00
parent d05ac13c27
commit be1cf7d978
5 changed files with 210 additions and 319 deletions
@@ -4,8 +4,6 @@ import expoParkOverviewImage from "@/assets/introduction/expo_park_open/open_00.
import sportsEmpowerHubImage from "@/assets/introduction/expo_park_open/open_01.jpg"; import sportsEmpowerHubImage from "@/assets/introduction/expo_park_open/open_01.jpg";
import fitnessResearchImage from "@/assets/introduction/expo_park_open/open_02.jpg"; import fitnessResearchImage from "@/assets/introduction/expo_park_open/open_02.jpg";
import expoParkZonesImage from "@/assets/introduction/expo_park_open/open_03.jpg"; import expoParkZonesImage from "@/assets/introduction/expo_park_open/open_03.jpg";
import Layout from "@/components/layout/DefaultLayout.vue";
import PageHeading from "@/components/layout/PageHeading.vue";
const slides = [ const slides = [
{ {
@@ -30,11 +28,7 @@ const activeSlide = shallowRef(0);
</script> </script>
<template> <template>
<Layout> <v-card class="introduction-card" variant="flat">
<template #main>
<PageHeading title="計畫簡介" />
<v-card variant="flat">
<div class="carousel-shell"> <div class="carousel-shell">
<v-carousel <v-carousel
v-model="activeSlide" v-model="activeSlide"
@@ -70,12 +64,14 @@ const activeSlide = shallowRef(0);
</div> </div>
</v-card> </v-card>
</template> </template>
</Layout>
</template>
<style scoped> <style scoped>
.introduction-card {
margin-bottom: 16px;
}
.introduction-carousel { .introduction-carousel {
height: calc(100dvh - 220px) !important; height: calc(100dvh - 120px) !important;
min-height: 420px; min-height: 420px;
max-height: 680px; max-height: 680px;
} }
@@ -96,13 +92,6 @@ const activeSlide = shallowRef(0);
object-position: center; object-position: center;
} }
@media (max-width: 600px) {
.introduction-carousel {
height: calc(100dvh - 180px) !important;
min-height: 520px;
}
}
.carousel-pagination { .carousel-pagination {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -156,6 +145,11 @@ const activeSlide = shallowRef(0);
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.introduction-carousel {
height: calc(100dvh - 100px) !important;
min-height: 520px;
}
.carousel-pagination { .carousel-pagination {
gap: 6px; gap: 6px;
margin-top: 10px; margin-top: 10px;
@@ -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>
@@ -42,7 +37,7 @@
<template #prepend> <template #prepend>
<v-avatar color="accent" size="48"> <v-avatar color="accent" size="48">
<span class="text-white font-weight-bold text-h6"> <span class="text-white font-weight-bold text-h6">
{{ (store.userProfile.name || "姓名").substring(0, 1) }} {{ (store.userProfile.name || '姓名').substring(0, 1) }}
</span> </span>
</v-avatar> </v-avatar>
</template> </template>
@@ -63,9 +58,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 />
@@ -78,12 +71,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>
@@ -91,15 +79,6 @@
<!-- 主要功能連結 --> <!-- 主要功能連結 -->
<v-list density="comfortable" nav> <v-list density="comfortable" nav>
<v-list-item
class="py-2"
color="primary"
exact
prepend-icon="mdi-view-dashboard"
title="首頁 / 主控台"
to="/"
/>
<v-list-item <v-list-item
v-if="store.isLoggedIn" v-if="store.isLoggedIn"
color="primary" color="primary"
@@ -110,7 +89,7 @@
/> />
<v-list-item <v-list-item
v-if="store.isLoggedIn" v-if="BODY_COMPOSITION_ENABLED && store.isLoggedIn"
color="primary" color="primary"
prepend-icon="mdi-scale-bathroom" prepend-icon="mdi-scale-bathroom"
title="身體數值分析" title="身體數值分析"
@@ -118,18 +97,11 @@
@click="drawer = false" @click="drawer = false"
/> />
<v-list-item
color="primary"
prepend-icon="mdi-information-outline"
title="計畫簡介"
to="/introduction"
/>
<v-list-item <v-list-item
v-if="SELF_ASSESSMENT_ENABLED" v-if="SELF_ASSESSMENT_ENABLED"
color="primary" color="primary"
prepend-icon="mdi-clipboard-text-search" prepend-icon="mdi-clipboard-text-search"
title="線上健康自評" title="自評"
to="/self-assessment" to="/self-assessment"
/> />
@@ -200,10 +172,7 @@
</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">
<slot name="main" /> <slot name="main" />
</v-container> </v-container>
@@ -220,17 +189,11 @@
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>
<v-btn <v-btn v-if="SELF_ASSESSMENT_ENABLED" to="/self-assessment" value="self-assessment">
v-if="SELF_ASSESSMENT_ENABLED"
to="/self-assessment"
value="self-assessment"
>
<v-icon :color="getBottomNavIconColor('self-assessment')"> <v-icon :color="getBottomNavIconColor('self-assessment')">
mdi-clipboard-text-search mdi-clipboard-text-search
</v-icon> </v-icon>
@@ -238,25 +201,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>
@@ -264,9 +215,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>
@@ -283,7 +232,7 @@
</ul> </ul>
<p class="text-caption text-grey"> <p class="text-caption text-grey">
授權狀態:{{ store.userProfile.consent ? "已同意授權" : "未授權" }} 授權狀態:{{ store.userProfile.consent ? '已同意授權' : '未授權' }}
</p> </p>
</v-card-text> </v-card-text>
@@ -303,57 +252,57 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from "vue"; import { ref, watch } from 'vue'
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from 'vue-router'
import { useDisplayUserName } from "@/composables/useDisplayUserName"; import { useDisplayUserName } from '@/composables/useDisplayUserName'
import { import {
APPOINTMENT_ENABLED, APPOINTMENT_ENABLED,
BODY_COMPOSITION_ENABLED,
BOTTOM_NAVIGATION_ENABLED, BOTTOM_NAVIGATION_ENABLED,
CHECKIN_ENABLED, CHECKIN_ENABLED,
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 displayUserName = useDisplayUserName(store.userProfile); const displayUserName = useDisplayUserName(store.userProfile)
const drawer = ref(false); const drawer = ref(false)
const activeTab = ref("home"); const activeTab = ref('home')
const showPrivacy = ref(false); const showPrivacy = ref(false)
// 無障礙狀態 (可持久化儲存於 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>
+7 -6
View File
@@ -1,6 +1,7 @@
export const APPOINTMENT_ENABLED = false; export const APPOINTMENT_ENABLED = false
export const BOTTOM_NAVIGATION_ENABLED = false; export const BODY_COMPOSITION_ENABLED = false
export const CHECKIN_ENABLED = false; export const BOTTOM_NAVIGATION_ENABLED = false
export const EXERCISE_DURATION_RECOMMENDATION_ENABLED = false; export const CHECKIN_ENABLED = false
export const RADAR_ENABLED = false; export const EXERCISE_DURATION_RECOMMENDATION_ENABLED = false
export const SELF_ASSESSMENT_ENABLED = false; export const RADAR_ENABLED = false
export const SELF_ASSESSMENT_ENABLED = true
+107 -159
View File
@@ -2,6 +2,8 @@
<Layout> <Layout>
<template #main> <template #main>
<template v-if="store.isLoggedIn"> <template v-if="store.isLoggedIn">
<IntroductionCarousel />
<!-- 頂部:我的健康力護照卡片 (Taipei Masters Hub) --> <!-- 頂部:我的健康力護照卡片 (Taipei Masters Hub) -->
<v-card class="border-primary border-xl rounded-xl" variant="flat"> <v-card class="border-primary border-xl rounded-xl" variant="flat">
<v-row class="mt-4 pa-2" no-gutters> <v-row class="mt-4 pa-2" no-gutters>
@@ -9,9 +11,7 @@
<v-card-title> <v-card-title>
{{ displayUserName }} {{ displayUserName }}
</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 class="d-flex justify-center" cols="4"> <v-col class="d-flex justify-center" cols="4">
@@ -23,9 +23,7 @@
'flex-column', 'flex-column',
'justify-center', 'justify-center',
'align-center', 'align-center',
courseStore.groupLoaded courseStore.groupLoaded ? `text-${courseStore.groupMeta.color}` : 'text-primary',
? `text-${courseStore.groupMeta.color}`
: 'text-primary',
]" ]"
> >
<v-progress-circular <v-progress-circular
@@ -48,13 +46,7 @@
<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>
@@ -108,11 +100,7 @@
<!-- 課程建議入口 --> <!-- 課程建議入口 -->
<v-list-item <v-list-item append-icon="mdi-chevron-right" class="bg-primary py-4 mt-4" to="/course">
append-icon="mdi-chevron-right"
class="bg-primary py-4 mt-4"
to="/course"
>
<v-list-item-title class="font-weight-bold"> <v-list-item-title class="font-weight-bold">
<v-icon icon="mdi-notebook-check-outline" size="16" /> <v-icon icon="mdi-notebook-check-outline" size="16" />
課程建議 課程建議
@@ -139,6 +127,7 @@
<HealthRadarChart :radar-data="store.radarData" /> <HealthRadarChart :radar-data="store.radarData" />
</v-card> </v-card>
<ModuleRouterCard <ModuleRouterCard
v-if="BODY_COMPOSITION_ENABLED"
description="查看 BMI 與體脂率的分析結果。" description="查看 BMI 與體脂率的分析結果。"
icon="mdi-scale-bathroom" icon="mdi-scale-bathroom"
icon-color="primary" icon-color="primary"
@@ -153,14 +142,6 @@
@refresh="loadPhysicalFitness" @refresh="loadPhysicalFitness"
/> />
<ModuleRouterCard
description="了解公益普測計畫、研究依據與平台流程。"
icon="mdi-information-outline"
icon-color="primary"
title="簡介"
to="/introduction"
/>
<ModuleRouterCard <ModuleRouterCard
v-if="SELF_ASSESSMENT_ENABLED" v-if="SELF_ASSESSMENT_ENABLED"
description="完成線上自評,取得初步風險分級。" description="完成線上自評,取得初步風險分級。"
@@ -206,7 +187,7 @@
<div class="text-left"> <div class="text-left">
<div class="text-title-medium font-weight-bold text-primary"> <div class="text-title-medium font-weight-bold text-primary">
無須登入,先做線上健康自評 無須登入,先做線上自評
</div> </div>
<div class="text-title-small text-grey mt-2"> <div class="text-title-small text-grey mt-2">
@@ -256,30 +237,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 <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>
<ModuleRouterCard
description="了解公益普測計畫、研究依據與平台流程。"
icon="mdi-information-outline"
icon-color="primary"
title="簡介"
to="/introduction"
/>
</template> </template>
<IntroductionCarousel v-if="!store.isLoggedIn" />
<PlatformIntroduction /> <PlatformIntroduction />
<v-dialog v-model="generateDialog" max-width="400"> <v-dialog v-model="generateDialog" max-width="400">
@@ -309,12 +277,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>
@@ -327,7 +290,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"
@@ -351,11 +314,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">
尚未取得體適能分組 尚未取得體適能分組
@@ -401,58 +360,57 @@
</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 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 HealthRadarChart from "@/components/HealthRadarChart.vue"; import ModuleRouterCard from '@/components/ModuleRouterCard.vue'
import Layout from "@/components/layout/DefaultLayout.vue"; import PlatformIntroduction from '@/components/PlatformIntroduction.vue'
import ModuleRouterCard from "@/components/ModuleRouterCard.vue"; import PhysicalFitnessSummary from '@/components/PhysicalFitnessSummary.vue'
import PlatformIntroduction from "@/components/PlatformIntroduction.vue"; import { useDisplayUserName } from '@/composables/useDisplayUserName'
import PhysicalFitnessSummary from "@/components/PhysicalFitnessSummary.vue";
import { useDisplayUserName } from "@/composables/useDisplayUserName";
import { import {
APPOINTMENT_ENABLED, APPOINTMENT_ENABLED,
BODY_COMPOSITION_ENABLED,
CHECKIN_ENABLED, CHECKIN_ENABLED,
RADAR_ENABLED, RADAR_ENABLED,
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'
import { getApiErrorMessage } from "@/utils/api-error"; import { getApiErrorMessage } from '@/utils/api-error'
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 displayUserName = useDisplayUserName(store.userProfile); const displayUserName = useDisplayUserName(store.userProfile)
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,仍允許本次彈窗正常使用。
} }
@@ -460,154 +418,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 -8
View File
@@ -5,12 +5,15 @@ import Checkin from "@/pages/checkin.vue";
import CheckinDetail from "@/pages/checkinDetail.vue"; import CheckinDetail from "@/pages/checkinDetail.vue";
import Course from "@/pages/course.vue"; import Course from "@/pages/course.vue";
import Index from "@/pages/index.vue"; import Index from "@/pages/index.vue";
import Introduction from "@/pages/introduction.vue";
import Login from "@/pages/login.vue"; import Login from "@/pages/login.vue";
import Profile from "@/pages/profile.vue"; import Profile from "@/pages/profile.vue";
import Register from "@/pages/register.vue"; import Register from "@/pages/register.vue";
import SelfAssessment from "@/pages/self-assessment.vue"; import SelfAssessment from "@/pages/self-assessment.vue";
import { APPOINTMENT_ENABLED, CHECKIN_ENABLED } from "@/config/features"; import {
APPOINTMENT_ENABLED,
BODY_COMPOSITION_ENABLED,
CHECKIN_ENABLED,
} from "@/config/features";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import { useCourseStore } from "@/stores/course"; import { useCourseStore } from "@/stores/course";
import { getApiErrorStatus } from "@/utils/api-error"; import { getApiErrorStatus } from "@/utils/api-error";
@@ -26,12 +29,6 @@ const router = createRouter({
name: "home", name: "home",
component: Index, component: Index,
}, },
{
path: "/introduction",
name: "introduction",
component: Introduction,
meta: { title: "計畫簡介|運動玩轉健康力" },
},
{ {
path: "/login", path: "/login",
name: "login", name: "login",
@@ -96,12 +93,14 @@ const router = createRouter({
}); });
const APPOINTMENT_PATH = /^\/appointment(?:\/|$)/; const APPOINTMENT_PATH = /^\/appointment(?:\/|$)/;
const BODY_COMPOSITION_PATH = /^\/body-composition(?:\/|$)/;
const CHECKIN_PATH = /^\/checkin(?:\/|$)/; const CHECKIN_PATH = /^\/checkin(?:\/|$)/;
// 簡單的導航守衛:只有標記需要登入的頁面會導向登入頁 // 簡單的導航守衛:只有標記需要登入的頁面會導向登入頁
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
if ( if (
(!APPOINTMENT_ENABLED && APPOINTMENT_PATH.test(to.path)) || (!APPOINTMENT_ENABLED && APPOINTMENT_PATH.test(to.path)) ||
(!BODY_COMPOSITION_ENABLED && BODY_COMPOSITION_PATH.test(to.path)) ||
(!CHECKIN_ENABLED && CHECKIN_PATH.test(to.path)) (!CHECKIN_ENABLED && CHECKIN_PATH.test(to.path))
) { ) {
return { name: "home" }; return { name: "home" };