From 3211c3b78b2d85ce895e7d18ddc348f886d94f20 Mon Sep 17 00:00:00 2001 From: skytek_xinliang Date: Tue, 6 Oct 2026 09:55:30 +0800 Subject: [PATCH] fix: classname --- .../src/components/PhysicalFitnessSummary.vue | 4 +- .../src/components/PrivacyConsentDialog.vue | 2 +- .../components/auth/PasswordRecoveryShell.vue | 2 +- .../src/components/layout/DefaultLayout.vue | 133 ++++++--- .../src/pages/appointment.vue | 6 +- app/citizen-frontend/src/pages/checkin.vue | 2 +- .../src/pages/checkinDetail.vue | 2 +- .../src/pages/complete-email.vue | 66 +++-- .../src/pages/forgot-password.vue | 46 ++- app/citizen-frontend/src/pages/index.vue | 95 +++--- app/citizen-frontend/src/pages/login.vue | 274 ++++++++---------- app/citizen-frontend/src/router/index.ts | 239 +++++++-------- 12 files changed, 471 insertions(+), 400 deletions(-) diff --git a/app/citizen-frontend/src/components/PhysicalFitnessSummary.vue b/app/citizen-frontend/src/components/PhysicalFitnessSummary.vue index fd6cd5e..54db074 100644 --- a/app/citizen-frontend/src/components/PhysicalFitnessSummary.vue +++ b/app/citizen-frontend/src/components/PhysicalFitnessSummary.vue @@ -61,12 +61,12 @@ class="test-item d-flex align-center justify-space-between rounded-lg px-3" >
-
+
{{ item.fitnessName }}
{{ item.fitnessScore ?? "無資料" }} diff --git a/app/citizen-frontend/src/components/PrivacyConsentDialog.vue b/app/citizen-frontend/src/components/PrivacyConsentDialog.vue index ff782f5..18facaa 100644 --- a/app/citizen-frontend/src/components/PrivacyConsentDialog.vue +++ b/app/citizen-frontend/src/components/PrivacyConsentDialog.vue @@ -12,7 +12,7 @@ const isOpen = defineModel({ required: true }); 個資同意與資料授權 - +

「運動玩轉健康力」平台非常重視您的個人隱私。我們所蒐集的身體活動自評、血壓、體脂以及現場運動表現數據,將僅用於以下用途:

diff --git a/app/citizen-frontend/src/components/auth/PasswordRecoveryShell.vue b/app/citizen-frontend/src/components/auth/PasswordRecoveryShell.vue index 9ea02be..8e70702 100644 --- a/app/citizen-frontend/src/components/auth/PasswordRecoveryShell.vue +++ b/app/citizen-frontend/src/components/auth/PasswordRecoveryShell.vue @@ -21,7 +21,7 @@ defineProps<{ title: string; description?: string }>(); {{ title }} -

+

{{ description }}

diff --git a/app/citizen-frontend/src/components/layout/DefaultLayout.vue b/app/citizen-frontend/src/components/layout/DefaultLayout.vue index 48c7623..e06cb02 100644 --- a/app/citizen-frontend/src/components/layout/DefaultLayout.vue +++ b/app/citizen-frontend/src/components/layout/DefaultLayout.vue @@ -6,7 +6,9 @@ - 運動玩轉健康力 + + 運動玩轉健康力 + @@ -61,7 +66,9 @@ - 未登入訪客 + 未登入訪客 @@ -74,7 +81,12 @@ 大字體模式 @@ -191,11 +203,16 @@ - + {{ EMAIL_COMPLETION_REQUIRED - ? '請先註冊 E-Mail,才能繼續使用平台。' - : '尚未設定 E-Mail,前往註冊以接收重要通知。' + ? "請先註冊 E-Mail,才能繼續使用平台。" + : "尚未設定 E-Mail,前往註冊以接收重要通知。" }} - + 前往補登
@@ -230,7 +252,9 @@ tag="div" > - mdi-view-dashboard + mdi-view-dashboard 首頁 @@ -246,13 +270,25 @@ 自評 - - mdi-qrcode-scan + + mdi-qrcode-scan 量測 - - mdi-calendar-check + + mdi-calendar-check 預約 @@ -260,12 +296,14 @@ - + 個資同意與資料授權 - +

「運動玩轉健康力」平台非常重視您的個人隱私。我們所蒐集的身體活動自評、血壓、體脂以及現場運動表現數據,將僅用於以下用途:

@@ -293,8 +331,8 @@ @@ -370,8 +409,8 @@ async function handleLogout() { line-height: unset !important; } .v-card-text, - .text-body-1, - .text-body-2, + .text-body-large, + .text-body-medium, p { font-size: 1.15rem !important; line-height: 1.6 !important; diff --git a/app/citizen-frontend/src/pages/appointment.vue b/app/citizen-frontend/src/pages/appointment.vue index 9ef8fb3..f29f719 100644 --- a/app/citizen-frontend/src/pages/appointment.vue +++ b/app/citizen-frontend/src/pages/appointment.vue @@ -87,7 +87,7 @@ size="18" /> -
+
{{ apt.date }} {{ apt.time }}
@@ -95,7 +95,7 @@
@@ -106,7 +106,7 @@ size="18" /> -
+
剩餘名額: -

+

您的健康力五維雷達圖、與同齡區間的參考值以及健康力總分已成功計算更新。

diff --git a/app/citizen-frontend/src/pages/checkinDetail.vue b/app/citizen-frontend/src/pages/checkinDetail.vue index 6da01b9..f3b41bf 100644 --- a/app/citizen-frontend/src/pages/checkinDetail.vue +++ b/app/citizen-frontend/src/pages/checkinDetail.vue @@ -191,7 +191,7 @@ -
+
{{ station.reference }}
diff --git a/app/citizen-frontend/src/pages/complete-email.vue b/app/citizen-frontend/src/pages/complete-email.vue index c0a9034..cf800b4 100644 --- a/app/citizen-frontend/src/pages/complete-email.vue +++ b/app/citizen-frontend/src/pages/complete-email.vue @@ -1,54 +1,58 @@ @@ -58,9 +62,13 @@ async function skipForNow() { - 註冊 E-Mail + + 註冊 E-Mail + -

填寫 E-Mail,有助於日後接收帳號安全與重要服務通知。

+

+ 填寫 E-Mail,有助於日後接收帳號安全與重要服務通知。 +

-import ForgotPasswordForm from '@/components/auth/ForgotPasswordForm.vue' -import PasswordRecoveryShell from '@/components/auth/PasswordRecoveryShell.vue' -import { useForgotPassword } from '@/composables/useForgotPassword' +import ForgotPasswordForm from "@/components/auth/ForgotPasswordForm.vue"; +import PasswordRecoveryShell from "@/components/auth/PasswordRecoveryShell.vue"; +import { useForgotPassword } from "@/composables/useForgotPassword"; const recoverySteps = [ - { title: '輸入已註冊手機門號', icon: 'mdi-phone' }, - { title: '收起E-Mail,使用連結進行重設密碼', icon: 'mdi-email-outline' }, -] + { title: "輸入已註冊手機門號", icon: "mdi-phone" }, + { title: "收起E-Mail,使用連結進行重設密碼", icon: "mdi-email-outline" }, +]; -const { accepted, loading, disabled, errorMessage, fieldErrors, remainingSeconds, submit } = - useForgotPassword() +const { + accepted, + loading, + disabled, + errorMessage, + fieldErrors, + remainingSeconds, + submit, +} = useForgotPassword(); @@ -317,10 +338,10 @@ -

+

預約普測,建立您的體適能分組

-

+

完成預約並到現場參加量測後,系統才能建立您的體適能分組與課程建議。

diff --git a/app/citizen-frontend/src/pages/login.vue b/app/citizen-frontend/src/pages/login.vue index 5e0cc98..ad1ac03 100644 --- a/app/citizen-frontend/src/pages/login.vue +++ b/app/citizen-frontend/src/pages/login.vue @@ -16,119 +16,137 @@
- - 登入 - 請輸入您的手機號碼與登入密碼 - - 密碼已更新,請使用新密碼登入。 - - - - + 登入 + +
+ + 密碼已更新,請使用新密碼登入。 + + + + {{ csrfRecoveryMessage }} + + + + {{ errorMessage }} + + + + + + + + {{ + remainingSeconds > 0 + ? `${remainingSeconds} 秒後再試` + : "登入健康平台" + }} + + + + 既有帳號建立密碼 + +
+ + + +
- {{ csrfRecoveryMessage }} - - - - {{ errorMessage }} - - - - - - - - {{ - remainingSeconds > 0 - ? `${remainingSeconds} 秒後再試` - : "登入健康平台" - }} - - - - 既有帳號建立密碼 - - - 忘記密碼 - + 無法登入? + + 忘記密碼 + +
- - - - - - + + 登入即表示您同意本平台的 個資授權條款 @@ -212,6 +230,8 @@ async function finishAuthentication() { } async function handleLogin() { + if (loading.value || remainingSeconds.value > 0) return; + const phone = normalizePhone(account.value); if (!/^09\d{8}$/.test(phone)) { @@ -258,41 +278,3 @@ async function _handleLineLogin() { } } - - diff --git a/app/citizen-frontend/src/router/index.ts b/app/citizen-frontend/src/router/index.ts index 1ff0765..8a6e21b 100644 --- a/app/citizen-frontend/src/router/index.ts +++ b/app/citizen-frontend/src/router/index.ts @@ -3,275 +3,280 @@ import { createWebHistory, isNavigationFailure, NavigationFailureType, -} 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 ForgotPassword from '@/pages/forgot-password.vue' -import ChangePassword from '@/pages/change-password.vue' +} 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 ForgotPassword from "@/pages/forgot-password.vue"; +import ChangePassword from "@/pages/change-password.vue"; import { captureInitialPasswordResetToken, clearPasswordResetToken, deliverPasswordResetToken, isPublicPasswordPath, stagePasswordResetToken, -} from '@/utils/password-reset-token' -import SelfAssessment from '@/pages/self-assessment.vue' +} from "@/utils/password-reset-token"; +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' +} from "@/config/features"; +import { useAppStore } from "@/stores/app"; +import { useCourseStore } from "@/stores/course"; +import { getApiErrorStatus } from "@/utils/api-error"; // 在 router 啟動初始導航與任何 Session API 之前移除敏感 query。 -captureInitialPasswordResetToken() +captureInitialPasswordResetToken(); const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), scrollBehavior(to, _from, savedPosition) { if (savedPosition) { - return savedPosition + return savedPosition; } if (to.hash) { - return { el: to.hash, top: 68 } + return { el: to.hash, top: 68 }; } - return { left: 0, top: 0 } + return { left: 0, top: 0 }; }, routes: [ { - path: '/', - name: 'home', + path: "/", + name: "home", component: Index, }, { - path: '/login', - name: 'login', + path: "/login", + name: "login", component: Login, }, { - path: '/forgot-password', - name: 'forgot-password', + path: "/forgot-password", + name: "forgot-password", component: ForgotPassword, - meta: { title: '忘記密碼|運動玩轉健康力' }, + meta: { title: "忘記密碼|運動玩轉健康力" }, }, { - path: '/change-password', - name: 'change-password', + path: "/change-password", + name: "change-password", component: ChangePassword, - meta: { title: '重設密碼|運動玩轉健康力' }, + meta: { title: "重設密碼|運動玩轉健康力" }, }, { - path: '/register', - name: 'register', + path: "/register", + name: "register", component: Register, - meta: { title: '註冊|運動玩轉健康力' }, + meta: { title: "註冊|運動玩轉健康力" }, }, { - path: '/self-assessment', - name: 'self-assessment', + path: "/self-assessment", + name: "self-assessment", component: SelfAssessment, meta: { requiresAuth: true }, }, { - path: '/complete-email', - name: 'complete-email', + path: "/complete-email", + name: "complete-email", component: CompleteEmail, meta: { requiresAuth: true, - title: '註冊 E-Mail|運動玩轉健康力', + title: "註冊 E-Mail|運動玩轉健康力", }, }, { - path: '/profile', - redirect: { name: 'home' }, + path: "/profile", + redirect: { name: "home" }, }, { - path: '/body-composition', - name: 'body-composition', + path: "/body-composition", + name: "body-composition", component: BodyComposition, meta: { requiresAuth: true, requiresUserID: true, - title: '身體數值分析|運動玩轉健康力', + title: "身體數值分析|運動玩轉健康力", }, }, { - path: '/appointment', - name: 'appointment', + path: "/appointment", + name: "appointment", component: Appointment, meta: { requiresAuth: true }, }, { - path: '/course', - name: 'course', + path: "/course", + name: "course", component: Course, meta: { requiresAuth: true, requiresFitnessGroup: true, - title: '課程建議|運動玩轉健康力', + title: "課程建議|運動玩轉健康力", }, }, { - path: '/checkin', - name: 'checkin', + path: "/checkin", + name: "checkin", component: Checkin, meta: { requiresAuth: true }, }, { - path: '/checkin/:code', - name: 'checkin-detail', + 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(?:\/|$)/ +const APPOINTMENT_PATH = /^\/appointment(?:\/|$)/; +const BODY_COMPOSITION_PATH = /^\/body-composition(?:\/|$)/; +const CHECKIN_PATH = /^\/checkin(?:\/|$)/; // 簡單的導航守衛:只有標記需要登入的頁面會導向登入頁 router.beforeEach(async (to) => { - if (to.name === 'change-password' && Object.hasOwn(to.query, 'token')) { - stagePasswordResetToken(to.query.token) - const query = { ...to.query } - delete query.token - return { name: 'change-password', query, hash: to.hash, replace: true } + if (to.name === "change-password" && Object.hasOwn(to.query, "token")) { + stagePasswordResetToken(to.query.token); + const query = { ...to.query }; + delete query.token; + return { name: "change-password", query, hash: to.hash, replace: true }; } // 公開密碼流程不依賴 Profile,也不受強制補登 E-Mail 限制。 - if (isPublicPasswordPath(to.path)) return + if (isPublicPasswordPath(to.path)) return; 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' } + return { name: "home" }; } - const store = useAppStore() - await store.initializeAuthentication() + const store = useAppStore(); + await store.initializeAuthentication(); if (to.meta.requiresAuth && !store.isLoggedIn) { return { - name: 'login', + name: "login", query: { redirect: to.fullPath }, - } + }; } if (to.meta.requiresUserID && !store.userID.trim()) { return { - name: 'login', + name: "login", query: { redirect: to.fullPath }, - } + }; } if ( EMAIL_COMPLETION_REQUIRED && store.isLoggedIn && store.requiresEmailCompletion && - to.name !== 'complete-email' + to.name !== "complete-email" ) { return { - name: 'complete-email', + name: "complete-email", query: { redirect: to.fullPath }, - } + }; } if (to.meta.requiresFitnessGroup) { - const courseStore = useCourseStore() + const courseStore = useCourseStore(); try { - await courseStore.ensureGroup() + await courseStore.ensureGroup(); } catch (error) { if (getApiErrorStatus(error) === 401 || !store.isLoggedIn) { return { - name: 'login', + name: "login", query: { redirect: to.fullPath }, - } + }; } if (courseStore.groupMissing) { return { - name: 'appointment', - query: { reason: 'missing-fitness-group' }, - } + name: "appointment", + query: { reason: "missing-fitness-group" }, + }; } // 技術性錯誤不應被誤導成尚未分組,返回首頁讓使用者重試。 - return { name: 'home' } + return { name: "home" }; } } -}) +}); router.afterEach((to, _from, failure) => { - if (!failure && to.name !== 'change-password') clearPasswordResetToken() + if (!failure && to.name !== "change-password") clearPasswordResetToken(); // 清理後的同路由導航可能被判定 duplicate,仍需移交新的 Token。 if ( - to.name === 'change-password' && + to.name === "change-password" && (!failure || isNavigationFailure(failure, NavigationFailureType.duplicated)) ) { - deliverPasswordResetToken() + deliverPasswordResetToken(); } - document.title = typeof to.meta.title === 'string' ? to.meta.title : '運動玩轉健康力' -}) + 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 (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' && !isPublicPasswordPath(currentRoute.path)) { + if ( + wasLoggedIn && + currentRoute.name !== "login" && + !isPublicPasswordPath(currentRoute.path) + ) { void router.replace({ - name: 'login', + name: "login", query: { redirect: currentRoute.fullPath }, - }) + }); } - }) + }); // CSRF Token 刷新並重送後仍失敗時,回到登入頁並提示使用者重新整理。 - window.addEventListener('csrf-recovery-required', () => { - const store = useAppStore() - const courseStore = useCourseStore() - const currentRoute = router.currentRoute.value + window.addEventListener("csrf-recovery-required", () => { + const store = useAppStore(); + const courseStore = useCourseStore(); + const currentRoute = router.currentRoute.value; if (isPublicPasswordPath(currentRoute.path)) { - store.clearAuthentication() - courseStore.resetGroup() - return + store.clearAuthentication(); + courseStore.resetGroup(); + return; } const redirect = - currentRoute.name === 'login' - ? String(currentRoute.query.redirect ?? '') - : currentRoute.fullPath - store.clearAuthentication() - courseStore.resetGroup() + currentRoute.name === "login" + ? String(currentRoute.query.redirect ?? "") + : currentRoute.fullPath; + store.clearAuthentication(); + courseStore.resetGroup(); void router.replace({ - name: 'login', + name: "login", query: { ...(redirect ? { redirect } : {}), - csrf: 'refresh', + csrf: "refresh", }, - }) - }) + }); + }); } -export default router +export default router;