import { createRouter, createWebHistory } from "vue-router"; import Appointment from "@/pages/appointment.vue"; import BodyComposition from "@/pages/body-composition.vue"; import Checkin from "@/pages/checkin.vue"; import CheckinDetail from "@/pages/checkinDetail.vue"; import CompleteEmail from "@/pages/complete-email.vue"; import Course from "@/pages/course.vue"; import Index from "@/pages/index.vue"; import Login from "@/pages/login.vue"; import Register from "@/pages/register.vue"; import SelfAssessment from "@/pages/self-assessment.vue"; import { APPOINTMENT_ENABLED, BODY_COMPOSITION_ENABLED, CHECKIN_ENABLED, EMAIL_COMPLETION_REQUIRED, } from "@/config/features"; import { useAppStore } from "@/stores/app"; import { useCourseStore } from "@/stores/course"; import { getApiErrorStatus } from "@/utils/api-error"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), scrollBehavior(to, _from, savedPosition) { if (savedPosition) { return savedPosition; } if (to.hash) { return { el: to.hash, top: 68 }; } return { left: 0, top: 0 }; }, routes: [ { path: "/", name: "home", component: Index, }, { path: "/login", name: "login", component: Login, }, { path: "/register", name: "register", component: Register, meta: { title: "註冊|運動玩轉健康力" }, }, { path: "/self-assessment", name: "self-assessment", component: SelfAssessment, meta: { requiresAuth: true }, }, { path: "/complete-email", name: "complete-email", component: CompleteEmail, meta: { requiresAuth: true, title: "補登 E-Mail|運動玩轉健康力", }, }, { path: "/profile", redirect: { name: "home" }, }, { path: "/body-composition", name: "body-composition", component: BodyComposition, meta: { requiresAuth: true, requiresUserID: true, title: "身體數值分析|運動玩轉健康力", }, }, { path: "/appointment", name: "appointment", component: Appointment, meta: { requiresAuth: true }, }, { path: "/course", name: "course", component: Course, meta: { requiresAuth: true, requiresFitnessGroup: true, title: "課程建議|運動玩轉健康力", }, }, { path: "/checkin", name: "checkin", component: Checkin, meta: { requiresAuth: true }, }, { path: "/checkin/:code", name: "checkin-detail", component: CheckinDetail, meta: { requiresAuth: true }, }, ], }); 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" }; } const store = useAppStore(); await store.initializeAuthentication(); if (to.meta.requiresAuth && !store.isLoggedIn) { return { name: "login", query: { redirect: to.fullPath }, }; } if (to.meta.requiresUserID && !store.userID.trim()) { return { name: "login", query: { redirect: to.fullPath }, }; } if ( EMAIL_COMPLETION_REQUIRED && store.isLoggedIn && store.requiresEmailCompletion && to.name !== "complete-email" ) { return { name: "complete-email", query: { redirect: to.fullPath }, }; } if (to.meta.requiresFitnessGroup) { const courseStore = useCourseStore(); try { await courseStore.ensureGroup(); } catch (error) { if (getApiErrorStatus(error) === 401 || !store.isLoggedIn) { return { name: "login", query: { redirect: to.fullPath }, }; } if (courseStore.groupMissing) { return { name: "appointment", query: { reason: "missing-fitness-group" }, }; } // 技術性錯誤不應被誤導成尚未分組,返回首頁讓使用者重試。 return { name: "home" }; } } }); router.afterEach((to) => { document.title = typeof to.meta.title === "string" ? to.meta.title : "運動玩轉健康力"; }); // 共用 HTTP client 收到登入後 API 的 401 時,清除前端狀態並保留原頁面供登入後返回。 if (typeof window !== "undefined") { window.addEventListener("auth-expired", () => { const store = useAppStore(); const courseStore = useCourseStore(); const currentRoute = router.currentRoute.value; const wasLoggedIn = store.isLoggedIn; store.clearAuthentication(); courseStore.resetGroup(); if (wasLoggedIn && currentRoute.name !== "login") { void router.replace({ name: "login", query: { redirect: currentRoute.fullPath }, }); } }); // CSRF Token 刷新並重送後仍失敗時,回到登入頁並提示使用者重新整理。 window.addEventListener("csrf-recovery-required", () => { const store = useAppStore(); const courseStore = useCourseStore(); const currentRoute = router.currentRoute.value; const redirect = currentRoute.name === "login" ? String(currentRoute.query.redirect ?? "") : currentRoute.fullPath; store.clearAuthentication(); courseStore.resetGroup(); void router.replace({ name: "login", query: { ...(redirect ? { redirect } : {}), csrf: "refresh", }, }); }); } export default router;