Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e8c184ec4 | |||
| bcaaf8797c | |||
| a6042b993a | |||
| 17dc3df663 | |||
| 7abe650757 |
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>
|
||||
<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>
|
||||
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ store.userProfile.name || "市民" }} 先生
|
||||
{{ displayUserName }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="text-white-50">{{
|
||||
@@ -132,7 +132,7 @@
|
||||
/>
|
||||
|
||||
<v-list-item
|
||||
v-if="store.isLoggedIn"
|
||||
v-if="APPOINTMENT_ENABLED && store.isLoggedIn"
|
||||
color="primary"
|
||||
prepend-icon="mdi-calendar-check"
|
||||
title="普測活動預約"
|
||||
@@ -140,7 +140,7 @@
|
||||
/>
|
||||
|
||||
<v-list-item
|
||||
v-if="store.isLoggedIn"
|
||||
v-if="CHECKIN_ENABLED && store.isLoggedIn"
|
||||
color="primary"
|
||||
prepend-icon="mdi-qrcode-scan"
|
||||
title="現場報到與量測"
|
||||
@@ -219,14 +219,22 @@
|
||||
<span class="bottom-nav-text">自評</span>
|
||||
</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')"
|
||||
>mdi-qrcode-scan</v-icon
|
||||
>
|
||||
<span class="bottom-nav-text">量測</span>
|
||||
</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')"
|
||||
>mdi-calendar-check</v-icon
|
||||
>
|
||||
@@ -250,7 +258,7 @@
|
||||
</p>
|
||||
|
||||
<ul class="pl-4 mb-3">
|
||||
<li>提供個人化的運動處方與健康改善建議。</li>
|
||||
<li>提供個人化的運動與健康改善建議。</li>
|
||||
<li>於普測活動現場合對您的身分。</li>
|
||||
<li>在去識別化的安全機制下,作為高齡健康促進的政策分析參考。</li>
|
||||
</ul>
|
||||
@@ -278,6 +286,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
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 { useCourseStore } from "@/stores/course";
|
||||
|
||||
@@ -285,6 +295,7 @@ 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");
|
||||
|
||||
@@ -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-subtitle class="text-grey-darken-3">
|
||||
報名免費體能普測,取得個人運動處方
|
||||
報名免費體能普測,取得個人運動建議
|
||||
</v-card-subtitle>
|
||||
</v-card>
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey-darken-2 font-weight-bold mb-4">
|
||||
姓名: {{ store.userProfile.name || "市民" }} 先生 | 電話:
|
||||
姓名: {{ displayUserName }} | 電話:
|
||||
{{ store.userProfile.phone }}
|
||||
</div>
|
||||
|
||||
@@ -262,6 +262,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useDisplayUserName } from "@/composables/useDisplayUserName";
|
||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
||||
import QRCode from "@/components/QRCode.vue";
|
||||
import { type Appointment, useAppStore } from "@/stores/app";
|
||||
@@ -273,6 +274,7 @@ const filterTab = ref("all");
|
||||
const showMissingGroupNotice = ref(
|
||||
route.query.reason === "missing-fitness-group",
|
||||
);
|
||||
const displayUserName = useDisplayUserName(store.userProfile);
|
||||
|
||||
const qrcodeDialog = ref(false);
|
||||
const selectedApt = ref<Appointment | null>(null);
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title class="font-weight-bold">
|
||||
精準運動處方建議
|
||||
精準運動建議
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
@@ -356,13 +356,6 @@
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</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>
|
||||
|
||||
@@ -402,11 +395,8 @@
|
||||
</template>
|
||||
|
||||
<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 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 { COURSE_DETAIL } from "@/data/courseDetail";
|
||||
import { useCourseStore } from "@/stores/course";
|
||||
@@ -423,14 +413,6 @@ const openPanels = ref([0]);
|
||||
const durationTablePanel = ref<string | null>("duration");
|
||||
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 即可
|
||||
const SHOW_SLOT_FILTER = false;
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
'justify-center',
|
||||
'align-center',
|
||||
courseStore.groupLoaded
|
||||
? `bg-${courseStore.groupMeta.color}`
|
||||
: 'bg-primary',
|
||||
? `text-${courseStore.groupMeta.color}`
|
||||
: 'text-primary',
|
||||
]"
|
||||
>
|
||||
<v-progress-circular
|
||||
v-if="courseStore.groupLoading"
|
||||
color="white"
|
||||
color="primary"
|
||||
indeterminate
|
||||
size="28"
|
||||
width="3"
|
||||
@@ -39,7 +39,7 @@
|
||||
{{ courseStore.groupMeta.label }}
|
||||
</span>
|
||||
<span v-else-if="courseStore.groupMissing" class="group-label">
|
||||
尚未分組
|
||||
未取得體適能分組
|
||||
</span>
|
||||
<span v-else class="group-label">無資料</span>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@
|
||||
</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">
|
||||
<div class="">肌力</div>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
<v-list-item
|
||||
append-icon="mdi-chevron-right"
|
||||
class="bg-primary py-4"
|
||||
class="bg-primary py-4 mt-4"
|
||||
to="/course"
|
||||
>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
@@ -113,7 +113,11 @@
|
||||
</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-subtitle>
|
||||
@@ -215,7 +219,7 @@
|
||||
</div>
|
||||
|
||||
<div class="text-title-small text-grey">
|
||||
普測預約、現場報到、量測紀錄與運動處方。
|
||||
普測預約、現場報到、量測紀錄與運動建議
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,6 +287,8 @@ 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 { useDisplayUserName } from "@/composables/useDisplayUserName";
|
||||
import { RADAR_ENABLED } from "@/config/features";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCourseStore } from "@/stores/course";
|
||||
|
||||
@@ -291,18 +297,7 @@ const courseStore = useCourseStore();
|
||||
const router = useRouter();
|
||||
const missingGroupDialog = ref(false);
|
||||
|
||||
const displayUserName = computed(() => {
|
||||
const name = store.userProfile.name || "市民";
|
||||
const gender = store.userProfile.gender;
|
||||
const honorific =
|
||||
gender === "男" || gender === "male"
|
||||
? "先生"
|
||||
: gender === "女" || gender === "female"
|
||||
? "女士"
|
||||
: "";
|
||||
|
||||
return honorific ? `${name} ${honorific}` : name;
|
||||
});
|
||||
const displayUserName = useDisplayUserName(store.userProfile);
|
||||
|
||||
const MISSING_GROUP_DIALOG_KEY = "missing-fitness-group-dialog-shown";
|
||||
|
||||
@@ -424,12 +419,15 @@ const todayTimeString = computed(() => {
|
||||
}
|
||||
|
||||
.score-disk {
|
||||
border: 3px solid #ffffff;
|
||||
width: min(100%, 108px);
|
||||
border: 2px solid currentColor;
|
||||
background-color: transparent;
|
||||
width: fit-content;
|
||||
min-width: 108px;
|
||||
max-width: 100%;
|
||||
height: 72px;
|
||||
padding: 0 10px;
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
padding: 0 8px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 3px 10px rgba(29, 112, 99, 0.12);
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -438,8 +436,11 @@ const todayTimeString = computed(() => {
|
||||
font-size: clamp(0.95rem, 3vw, 1.1rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
color: inherit;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.transition-card {
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import coverImage from "@/assets/introduction/functional _fitness_all_ages/00_cover.jpg";
|
||||
import overviewImage01 from "@/assets/introduction/functional _fitness_all_ages/01_overview.jpg";
|
||||
import methodsImage from "@/assets/introduction/functional _fitness_all_ages/02_methods.jpg";
|
||||
import resultsImage from "@/assets/introduction/functional _fitness_all_ages/03_results.jpg";
|
||||
import overviewImage02 from "@/assets/introduction/functional _fitness_all_ages/04_overview.jpg";
|
||||
import expoParkOverviewImage from "@/assets/introduction/expo_park/expo_00.png";
|
||||
import sportsEmpowerHubImage from "@/assets/introduction/expo_park/expo_01.png";
|
||||
import fitnessResearchImage from "@/assets/introduction/expo_park/expo_02.png";
|
||||
import expoParkZonesImage from "@/assets/introduction/expo_park/expo_03.png";
|
||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
||||
|
||||
const slides = [
|
||||
{
|
||||
title: "封面",
|
||||
src: coverImage,
|
||||
title: "計畫總覽",
|
||||
src: expoParkOverviewImage,
|
||||
},
|
||||
{
|
||||
title: "研究總覽",
|
||||
src: overviewImage01,
|
||||
title: "運動玩轉健康力理念",
|
||||
src: sportsEmpowerHubImage,
|
||||
},
|
||||
{
|
||||
title: "方法與指標",
|
||||
src: methodsImage,
|
||||
title: "全齡功能性體適能研究",
|
||||
src: fitnessResearchImage,
|
||||
},
|
||||
{
|
||||
title: "主要結果",
|
||||
src: resultsImage,
|
||||
},
|
||||
{
|
||||
title: "研究總覽",
|
||||
src: overviewImage02,
|
||||
title: "花博運動賦能基地分區",
|
||||
src: expoParkZonesImage,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 登入卡片 -->
|
||||
<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-form class="pa-4" @submit.prevent="handleLogin">
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
</template>
|
||||
|
||||
<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 type { UserProfile } from "@/stores/app";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
@@ -305,6 +305,11 @@ function validateForm() {
|
||||
return "";
|
||||
}
|
||||
|
||||
async function scrollToPageTop() {
|
||||
await nextTick();
|
||||
window.scrollTo({ top: 0 });
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
errorMessage.value = "";
|
||||
successMessage.value = "";
|
||||
@@ -312,6 +317,7 @@ async function handleSubmit() {
|
||||
const validationError = validateForm();
|
||||
if (validationError) {
|
||||
errorMessage.value = validationError;
|
||||
await scrollToPageTop();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,6 +344,8 @@ async function handleSubmit() {
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
await scrollToPageTop();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="text-center">
|
||||
<BrandAvatar />
|
||||
<v-card-title class="text-primary font-weight-bold">
|
||||
建立市民帳號
|
||||
建立帳號
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="text-primary">
|
||||
完成註冊後即可保存自評並進行預約
|
||||
|
||||
@@ -421,7 +421,7 @@
|
||||
class="text-body-medium text-grey-darken-3 mb-2"
|
||||
>
|
||||
您的身體功能與日常活動習慣良好。建議您每週繼續保持 3
|
||||
次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動處方。
|
||||
次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動建議。
|
||||
</p>
|
||||
|
||||
<p
|
||||
|
||||
@@ -9,6 +9,7 @@ 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 { useAppStore } from "@/stores/app";
|
||||
import { useCourseStore } from "@/stores/course";
|
||||
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) => {
|
||||
if (
|
||||
(!APPOINTMENT_ENABLED && APPOINTMENT_PATH.test(to.path)) ||
|
||||
(!CHECKIN_ENABLED && CHECKIN_PATH.test(to.path))
|
||||
) {
|
||||
return { name: "home" };
|
||||
}
|
||||
|
||||
const store = useAppStore();
|
||||
await store.initializeAuthentication();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user