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