Compare commits

...

5 Commits

Author SHA1 Message Date
skytek_xinliang 3e8c184ec4 feat: 更換簡介圖片 2026-09-14 09:48:31 +08:00
skytek_xinliang bcaaf8797c feat: feature switch 2026-09-14 09:35:56 +08:00
skytek_xinliang a6042b993a fix: 字樣改成方針 2026-09-14 09:09:06 +08:00
skytek_xinliang 17dc3df663 fix: 移除市民字樣 2026-09-14 08:56:22 +08:00
skytek_xinliang 7abe650757 fix: 個人資料修改後滾動至上方 2026-08-31 14:37:27 +08:00
16 changed files with 112 additions and 77 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

@@ -42,13 +42,13 @@
<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>
<v-list-item-title class="font-weight-bold"> <v-list-item-title class="font-weight-bold">
{{ store.userProfile.name || "市民" }} 先生 {{ displayUserName }}
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle class="text-white-50">{{ <v-list-item-subtitle class="text-white-50">{{
@@ -132,7 +132,7 @@
/> />
<v-list-item <v-list-item
v-if="store.isLoggedIn" v-if="APPOINTMENT_ENABLED && store.isLoggedIn"
color="primary" color="primary"
prepend-icon="mdi-calendar-check" prepend-icon="mdi-calendar-check"
title="普測活動預約" title="普測活動預約"
@@ -140,7 +140,7 @@
/> />
<v-list-item <v-list-item
v-if="store.isLoggedIn" v-if="CHECKIN_ENABLED && store.isLoggedIn"
color="primary" color="primary"
prepend-icon="mdi-qrcode-scan" prepend-icon="mdi-qrcode-scan"
title="現場報到與量測" title="現場報到與量測"
@@ -219,14 +219,22 @@
<span class="bottom-nav-text">自評</span> <span class="bottom-nav-text">自評</span>
</v-btn> </v-btn>
<v-btn v-if="store.isLoggedIn" to="/checkin" value="checkin"> <v-btn
v-if="CHECKIN_ENABLED && store.isLoggedIn"
to="/checkin"
value="checkin"
>
<v-icon :color="getBottomNavIconColor('checkin')" <v-icon :color="getBottomNavIconColor('checkin')"
>mdi-qrcode-scan</v-icon >mdi-qrcode-scan</v-icon
> >
<span class="bottom-nav-text">量測</span> <span class="bottom-nav-text">量測</span>
</v-btn> </v-btn>
<v-btn v-if="store.isLoggedIn" to="/appointment" value="appointment"> <v-btn
v-if="APPOINTMENT_ENABLED && store.isLoggedIn"
to="/appointment"
value="appointment"
>
<v-icon :color="getBottomNavIconColor('appointment')" <v-icon :color="getBottomNavIconColor('appointment')"
>mdi-calendar-check</v-icon >mdi-calendar-check</v-icon
> >
@@ -250,7 +258,7 @@
</p> </p>
<ul class="pl-4 mb-3"> <ul class="pl-4 mb-3">
<li>提供個人化的運動處方與健康改善建議。</li> <li>提供個人化的運動與健康改善建議。</li>
<li>於普測活動現場合對您的身分。</li> <li>於普測活動現場合對您的身分。</li>
<li>在去識別化的安全機制下,作為高齡健康促進的政策分析參考。</li> <li>在去識別化的安全機制下,作為高齡健康促進的政策分析參考。</li>
</ul> </ul>
@@ -278,6 +286,8 @@
<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 { APPOINTMENT_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";
@@ -285,6 +295,7 @@ 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 drawer = ref(false); const drawer = ref(false);
const activeTab = ref("home"); const activeTab = ref("home");
@@ -0,0 +1,22 @@
import { computed, type ComputedRef } from "vue";
interface UserProfileName {
name: string;
gender: string;
}
export function useDisplayUserName(
userProfile: UserProfileName,
): ComputedRef<string> {
return computed(() => {
const name = userProfile.name || "姓名";
const honorific =
userProfile.gender === "男" || userProfile.gender === "male"
? "先生"
: userProfile.gender === "女" || userProfile.gender === "female"
? "女士"
: "";
return honorific ? `${name} ${honorific}` : name;
});
}
@@ -0,0 +1,3 @@
export const APPOINTMENT_ENABLED = false;
export const CHECKIN_ENABLED = false;
export const RADAR_ENABLED = false;
@@ -5,7 +5,7 @@
<v-card-title class="text-primary">預約</v-card-title> <v-card-title class="text-primary">預約</v-card-title>
<v-card-subtitle class="text-grey-darken-3"> <v-card-subtitle class="text-grey-darken-3">
報名免費體能普測,取得個人運動處方 報名免費體能普測,取得個人運動建議
</v-card-subtitle> </v-card-subtitle>
</v-card> </v-card>
@@ -224,7 +224,7 @@
</div> </div>
<div class="text-caption text-grey-darken-2 font-weight-bold mb-4"> <div class="text-caption text-grey-darken-2 font-weight-bold mb-4">
姓名: {{ store.userProfile.name || "市民" }} 先生 | 電話: 姓名: {{ displayUserName }} | 電話:
{{ store.userProfile.phone }} {{ store.userProfile.phone }}
</div> </div>
@@ -262,6 +262,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useDisplayUserName } from "@/composables/useDisplayUserName";
import Layout from "@/components/layout/DefaultLayout.vue"; import Layout from "@/components/layout/DefaultLayout.vue";
import QRCode from "@/components/QRCode.vue"; import QRCode from "@/components/QRCode.vue";
import { type Appointment, useAppStore } from "@/stores/app"; import { type Appointment, useAppStore } from "@/stores/app";
@@ -273,6 +274,7 @@ const filterTab = ref("all");
const showMissingGroupNotice = ref( const showMissingGroupNotice = ref(
route.query.reason === "missing-fitness-group", route.query.reason === "missing-fitness-group",
); );
const displayUserName = useDisplayUserName(store.userProfile);
const qrcodeDialog = ref(false); const qrcodeDialog = ref(false);
const selectedApt = ref<Appointment | null>(null); const selectedApt = ref<Appointment | null>(null);
+2 -20
View File
@@ -161,7 +161,7 @@
<v-expansion-panel> <v-expansion-panel>
<v-expansion-panel-title class="font-weight-bold"> <v-expansion-panel-title class="font-weight-bold">
精準運動處方建議 精準運動建議
</v-expansion-panel-title> </v-expansion-panel-title>
<v-expansion-panel-text> <v-expansion-panel-text>
@@ -356,13 +356,6 @@
</v-expansion-panel> </v-expansion-panel>
</v-expansion-panels> </v-expansion-panels>
</v-card> </v-card>
<v-card class="rounded-lg elevation-1 mt-4 overflow-hidden">
<v-img
:alt="`${store.groupMeta.label}課表示意圖`"
:src="groupIllustration"
/>
</v-card>
</template> </template>
</template> </template>
@@ -402,11 +395,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FitnessGroup, TimeSlot, YesNo } from "@/api/types"; import type { TimeSlot, YesNo } from "@/api/types";
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import highIllustration from "@/assets/course/high.png";
import lowIllustration from "@/assets/course/low.png";
import mediumIllustration from "@/assets/course/medium.png";
import Layout from "@/components/layout/DefaultLayout.vue"; import Layout from "@/components/layout/DefaultLayout.vue";
import { COURSE_DETAIL } from "@/data/courseDetail"; import { COURSE_DETAIL } from "@/data/courseDetail";
import { useCourseStore } from "@/stores/course"; import { useCourseStore } from "@/stores/course";
@@ -423,14 +413,6 @@ const openPanels = ref([0]);
const durationTablePanel = ref<string | null>("duration"); const durationTablePanel = ref<string | null>("duration");
const scheduleTablePanel = ref<string | null>("schedule"); const scheduleTablePanel = ref<string | null>("schedule");
const GROUP_ILLUSTRATIONS: Record<FitnessGroup, string> = {
low: lowIllustration,
medium: mediumIllustration,
high: highIllustration,
};
const groupIllustration = computed(() => GROUP_ILLUSTRATIONS[store.group]);
// 時段篩選功能尚未開放,UI 先隱藏;之後要開放時把這個常數改回 true 即可 // 時段篩選功能尚未開放,UI 先隱藏;之後要開放時把這個常數改回 true 即可
const SHOW_SLOT_FILTER = false; const SHOW_SLOT_FILTER = false;
+28 -27
View File
@@ -24,13 +24,13 @@
'justify-center', 'justify-center',
'align-center', 'align-center',
courseStore.groupLoaded courseStore.groupLoaded
? `bg-${courseStore.groupMeta.color}` ? `text-${courseStore.groupMeta.color}`
: 'bg-primary', : 'text-primary',
]" ]"
> >
<v-progress-circular <v-progress-circular
v-if="courseStore.groupLoading" v-if="courseStore.groupLoading"
color="white" color="primary"
indeterminate indeterminate
size="28" size="28"
width="3" width="3"
@@ -39,7 +39,7 @@
{{ courseStore.groupMeta.label }} {{ courseStore.groupMeta.label }}
</span> </span>
<span v-else-if="courseStore.groupMissing" class="group-label"> <span v-else-if="courseStore.groupMissing" class="group-label">
尚未分組 未取得體適能分組
</span> </span>
<span v-else class="group-label">無資料</span> <span v-else class="group-label">無資料</span>
</div> </div>
@@ -60,7 +60,7 @@
</v-card-subtitle> </v-card-subtitle>
<!-- 四項核心能力數值簡覽 --> <!-- 四項核心能力數值簡覽 -->
<v-row class="mt-4" no-gutters> <v-row v-if="RADAR_ENABLED" class="mt-4" no-gutters>
<v-col class="text-center border pa-2" cols="3"> <v-col class="text-center border pa-2" cols="3">
<div class="">肌力</div> <div class="">肌力</div>
@@ -98,7 +98,7 @@
<v-list-item <v-list-item
append-icon="mdi-chevron-right" append-icon="mdi-chevron-right"
class="bg-primary py-4" class="bg-primary py-4 mt-4"
to="/course" to="/course"
> >
<v-list-item-title class="font-weight-bold"> <v-list-item-title class="font-weight-bold">
@@ -113,7 +113,11 @@
</v-card> </v-card>
<!-- 中間:功能指標與五維雷達圖 --> <!-- 中間:功能指標與五維雷達圖 -->
<v-card class="border-xl border-primary rounded-xl mt-4" variant="flat"> <v-card
v-if="RADAR_ENABLED"
class="border-xl border-primary rounded-xl mt-4"
variant="flat"
>
<v-card-title> 個人五維健康力指標 </v-card-title> <v-card-title> 個人五維健康力指標 </v-card-title>
<v-card-subtitle> <v-card-subtitle>
@@ -215,7 +219,7 @@
</div> </div>
<div class="text-title-small text-grey"> <div class="text-title-small text-grey">
普測預約、現場報到、量測紀錄與運動處方。 普測預約、現場報到、量測紀錄與運動建議
</div> </div>
</div> </div>
</div> </div>
@@ -283,6 +287,8 @@ import BrandAvatar from "@/components/BrandAvatar.vue";
import HealthRadarChart from "@/components/HealthRadarChart.vue"; import HealthRadarChart from "@/components/HealthRadarChart.vue";
import Layout from "@/components/layout/DefaultLayout.vue"; import Layout from "@/components/layout/DefaultLayout.vue";
import ModuleRouterCard from "@/components/ModuleRouterCard.vue"; import ModuleRouterCard from "@/components/ModuleRouterCard.vue";
import { useDisplayUserName } from "@/composables/useDisplayUserName";
import { RADAR_ENABLED } from "@/config/features";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import { useCourseStore } from "@/stores/course"; import { useCourseStore } from "@/stores/course";
@@ -291,18 +297,7 @@ const courseStore = useCourseStore();
const router = useRouter(); const router = useRouter();
const missingGroupDialog = ref(false); const missingGroupDialog = ref(false);
const displayUserName = computed(() => { const displayUserName = useDisplayUserName(store.userProfile);
const name = store.userProfile.name || "市民";
const gender = store.userProfile.gender;
const honorific =
gender === "男" || gender === "male"
? "先生"
: gender === "女" || gender === "female"
? "女士"
: "";
return honorific ? `${name} ${honorific}` : name;
});
const MISSING_GROUP_DIALOG_KEY = "missing-fitness-group-dialog-shown"; const MISSING_GROUP_DIALOG_KEY = "missing-fitness-group-dialog-shown";
@@ -424,12 +419,15 @@ const todayTimeString = computed(() => {
} }
.score-disk { .score-disk {
border: 3px solid #ffffff; border: 2px solid currentColor;
width: min(100%, 108px); background-color: transparent;
width: fit-content;
min-width: 108px;
max-width: 100%;
height: 72px; height: 72px;
padding: 0 10px; padding: 0 8px;
border-radius: 22px; border-radius: 16px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 3px 10px rgba(29, 112, 99, 0.12);
box-sizing: border-box; box-sizing: border-box;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -438,8 +436,11 @@ const todayTimeString = computed(() => {
font-size: clamp(0.95rem, 3vw, 1.1rem); font-size: clamp(0.95rem, 3vw, 1.1rem);
font-weight: 900; font-weight: 900;
line-height: 1; line-height: 1;
color: #ffffff; color: inherit;
white-space: nowrap; max-width: 100%;
overflow-wrap: anywhere;
text-align: center;
white-space: normal;
} }
.transition-card { .transition-card {
+12 -17
View File
@@ -1,31 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import coverImage from "@/assets/introduction/functional _fitness_all_ages/00_cover.jpg"; import expoParkOverviewImage from "@/assets/introduction/expo_park/expo_00.png";
import overviewImage01 from "@/assets/introduction/functional _fitness_all_ages/01_overview.jpg"; import sportsEmpowerHubImage from "@/assets/introduction/expo_park/expo_01.png";
import methodsImage from "@/assets/introduction/functional _fitness_all_ages/02_methods.jpg"; import fitnessResearchImage from "@/assets/introduction/expo_park/expo_02.png";
import resultsImage from "@/assets/introduction/functional _fitness_all_ages/03_results.jpg"; import expoParkZonesImage from "@/assets/introduction/expo_park/expo_03.png";
import overviewImage02 from "@/assets/introduction/functional _fitness_all_ages/04_overview.jpg";
import Layout from "@/components/layout/DefaultLayout.vue"; import Layout from "@/components/layout/DefaultLayout.vue";
const slides = [ const slides = [
{ {
title: "封面", title: "計畫總覽",
src: coverImage, src: expoParkOverviewImage,
}, },
{ {
title: "研究總覽", title: "運動玩轉健康力理念",
src: overviewImage01, src: sportsEmpowerHubImage,
}, },
{ {
title: "方法與指標", title: "全齡功能性體適能研究",
src: methodsImage, src: fitnessResearchImage,
}, },
{ {
title: "主要結果", title: "花博運動賦能基地分區",
src: resultsImage, src: expoParkZonesImage,
},
{
title: "研究總覽",
src: overviewImage02,
}, },
]; ];
</script> </script>
+1 -1
View File
@@ -17,7 +17,7 @@
<!-- 登入卡片 --> <!-- 登入卡片 -->
<v-card class="rounded-lg mt-4" color="surface"> <v-card class="rounded-lg mt-4" color="surface">
<v-card-title> 市民登入 </v-card-title> <v-card-title> 登入 </v-card-title>
<v-card-text> 請輸入您的手機門號與登入密碼 </v-card-text> <v-card-text> 請輸入您的手機門號與登入密碼 </v-card-text>
<!-- 表單 --> <!-- 表單 -->
<v-form class="pa-4" @submit.prevent="handleLogin"> <v-form class="pa-4" @submit.prevent="handleLogin">
+9 -1
View File
@@ -200,7 +200,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue"; import { computed, nextTick, onMounted, reactive, ref } from "vue";
import Layout from "@/components/layout/DefaultLayout.vue"; import Layout from "@/components/layout/DefaultLayout.vue";
import type { UserProfile } from "@/stores/app"; import type { UserProfile } from "@/stores/app";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
@@ -305,6 +305,11 @@ function validateForm() {
return ""; return "";
} }
async function scrollToPageTop() {
await nextTick();
window.scrollTo({ top: 0 });
}
async function handleSubmit() { async function handleSubmit() {
errorMessage.value = ""; errorMessage.value = "";
successMessage.value = ""; successMessage.value = "";
@@ -312,6 +317,7 @@ async function handleSubmit() {
const validationError = validateForm(); const validationError = validateForm();
if (validationError) { if (validationError) {
errorMessage.value = validationError; errorMessage.value = validationError;
await scrollToPageTop();
return; return;
} }
@@ -338,6 +344,8 @@ async function handleSubmit() {
} finally { } finally {
saving.value = false; saving.value = false;
} }
await scrollToPageTop();
} }
onMounted(async () => { onMounted(async () => {
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="text-center"> <div class="text-center">
<BrandAvatar /> <BrandAvatar />
<v-card-title class="text-primary font-weight-bold"> <v-card-title class="text-primary font-weight-bold">
建立市民帳號 建立帳號
</v-card-title> </v-card-title>
<v-card-subtitle class="text-primary"> <v-card-subtitle class="text-primary">
完成註冊後即可保存自評並進行預約 完成註冊後即可保存自評並進行預約
@@ -421,7 +421,7 @@
class="text-body-medium text-grey-darken-3 mb-2" class="text-body-medium text-grey-darken-3 mb-2"
> >
您的身體功能與日常活動習慣良好。建議您每週繼續保持 3 您的身體功能與日常活動習慣良好。建議您每週繼續保持 3
次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動處方。 次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動建議。
</p> </p>
<p <p
+11
View File
@@ -9,6 +9,7 @@ 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 { 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";
@@ -83,8 +84,18 @@ const router = createRouter({
], ],
}); });
const APPOINTMENT_PATH = /^\/appointment(?:\/|$)/;
const CHECKIN_PATH = /^\/checkin(?:\/|$)/;
// 簡單的導航守衛:只有標記需要登入的頁面會導向登入頁 // 簡單的導航守衛:只有標記需要登入的頁面會導向登入頁
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
if (
(!APPOINTMENT_ENABLED && APPOINTMENT_PATH.test(to.path)) ||
(!CHECKIN_ENABLED && CHECKIN_PATH.test(to.path))
) {
return { name: "home" };
}
const store = useAppStore(); const store = useAppStore();
await store.initializeAuthentication(); await store.initializeAuthentication();