Files
sports-empower-hub/app/citizen-frontend/src/router/index.ts
T
2026-10-05 10:11:04 +08:00

227 lines
5.8 KiB
TypeScript

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;