Files
sports-empower-hub/app/citizen-frontend/src/components/layout/DefaultLayout.vue
T
2026-06-23 09:59:44 +08:00

364 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<v-app :class="{ 'large-text-mode': isLargeText }">
<!-- 頂部導航欄 -->
<v-app-bar class="px-2 elevation-1" color="primary" flat>
<template #prepend>
<v-app-bar-nav-icon variant="text" @click="drawer = !drawer" />
</template>
<v-app-bar-title class="font-weight-bold flex-grow-1"> 運動玩轉健康力 </v-app-bar-title>
<template #append>
<div class="d-flex align-center">
<!-- 快捷無障礙切換 -->
<v-btn
class="mr-1"
icon
size="small"
title="切換大字體"
variant="text"
@click="isLargeText = !isLargeText"
>
<v-icon :color="isLargeText ? 'accent' : 'white'" icon="mdi-format-size" />
</v-btn>
</div>
</template>
</v-app-bar>
<!-- 側邊抽屜導航 -->
<v-navigation-drawer
v-model="drawer"
class="bg-background"
location="left"
temporary
width="300"
>
<v-list-item v-if="store.isLoggedIn" class="py-4 bg-primary text-white">
<template #prepend>
<v-avatar color="accent" size="48">
<span class="text-white font-weight-bold text-h6">{{
store.userProfile.name.substring(1)
}}</span>
</v-avatar>
</template>
<v-list-item-title class="font-weight-bold">
{{ store.userProfile.name }} 先生
</v-list-item-title>
<v-list-item-subtitle class="text-white-50">{{
store.userProfile.phone
}}</v-list-item-subtitle>
</v-list-item>
<v-list-item v-else class="py-2 bg-primary text-white">
<template #prepend>
<v-avatar color="accent" size="48">
<v-icon color="white" icon="mdi-heart-pulse" />
</v-avatar>
</template>
<v-list-item-subtitle class="text-white-50">未登入訪客</v-list-item-subtitle>
</v-list-item>
<v-divider />
<v-list-item>
<template #prepend>
<v-icon color="primary" icon="mdi-format-size" />
</template>
<v-list-item-title>大字體模式</v-list-item-title>
<template #append>
<v-switch v-model="isLargeText" color="secondary" density="compact" hide-details />
</template>
</v-list-item>
<v-divider class="my-2" />
<!-- 主要功能連結 -->
<v-list density="comfortable" nav>
<v-list-item
class="py-2"
color="primary"
exact
prepend-icon="mdi-view-dashboard"
title="首頁 / 主控台"
to="/"
/>
<v-list-item
color="primary"
prepend-icon="mdi-clipboard-text-search"
title="線上健康自評"
to="/self-assessment"
/>
<v-list-item
v-if="store.isLoggedIn"
color="primary"
prepend-icon="mdi-calendar-check"
title="普測活動預約"
to="/appointment"
/>
<v-list-item
v-if="store.isLoggedIn"
color="primary"
prepend-icon="mdi-qrcode-scan"
title="現場報到與量測"
to="/checkin"
/>
</v-list>
<template v-if="store.isLoggedIn">
<v-divider class="my-2" />
<v-list density="compact" nav>
<v-list-item
color="primary"
prepend-icon="mdi-shield-check"
title="資料授權與隱私條款"
@click="showPrivacy = true"
/>
<v-list-item
color="primary"
prepend-icon="mdi-phone"
title="緊急聯絡人資訊"
@click="showEmergency = true"
/>
</v-list>
</template>
<template #append>
<div class="pa-4">
<v-btn
v-if="store.isLoggedIn"
block
color="error"
prepend-icon="mdi-logout"
variant="tonal"
@click="handleLogout"
>
登出系統
</v-btn>
<v-btn
v-else
block
color="secondary"
prepend-icon="mdi-login"
to="/login"
variant="flat"
@click="drawer = false"
>
登入健康平台
</v-btn>
</div>
</template>
</v-navigation-drawer>
<!-- 主要內容區 -->
<v-main class="bg-white min-vh-100 pb-16">
<v-container class="px-4 py-4" fluid>
<slot name="main" />
</v-container>
</v-main>
<!-- 底部導航欄 (Mobile First 核心) -->
<v-bottom-navigation
v-model="activeTab"
bg-color="background"
color="primary-darken-1"
grow
tag="div"
>
<v-btn to="/" value="home">
<v-icon :color="getBottomNavIconColor('home')">mdi-view-dashboard</v-icon>
<span class="bottom-nav-text">首頁</span>
</v-btn>
<v-btn to="/self-assessment" value="self-assessment">
<v-icon :color="getBottomNavIconColor('self-assessment')">
mdi-clipboard-text-search
</v-icon>
<span class="bottom-nav-text">自評</span>
</v-btn>
<v-btn v-if="store.isLoggedIn" to="/appointment" value="appointment">
<v-icon :color="getBottomNavIconColor('appointment')">mdi-calendar-check</v-icon>
<span class="bottom-nav-text">預約</span>
</v-btn>
<v-btn v-if="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-bottom-navigation>
<!-- 隱私授權對話框 -->
<v-dialog v-model="showPrivacy" max-width="500">
<v-card class="rounded-xl">
<v-card-title class="bg-primary text-white font-weight-bold d-flex align-center">
<v-icon class="mr-2" icon="mdi-shield-check" />
個資同意與資料授權
</v-card-title>
<v-card-text class="pt-4 text-body-1">
<p class="mb-3">
運動玩轉健康力平台非常重視您的個人隱私我們所蒐集的身體活動自評血壓體脂以及現場運動表現數據將僅用於以下用途
</p>
<ul class="pl-4 mb-3">
<li>提供個人化的運動處方與健康改善建議</li>
<li>於普測活動現場合對您的身分</li>
<li>在去識別化的安全機制下作為高齡健康促進的政策分析參考</li>
</ul>
<p class="text-caption text-grey">
授權狀態{{ store.userProfile.consent ? '已同意授權' : '未授權' }}
</p>
</v-card-text>
<v-card-actions class="justify-end px-4 pb-4">
<v-btn
class="rounded-lg px-4"
color="primary"
variant="flat"
@click="showPrivacy = false"
>
確認關閉
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 緊急聯絡人對話框 -->
<v-dialog v-model="showEmergency" max-width="500">
<v-card class="rounded-xl">
<v-card-title class="bg-primary text-white font-weight-bold d-flex align-center">
<v-icon class="mr-2" icon="mdi-alert-circle-outline" />
緊急聯絡資訊
</v-card-title>
<v-card-text class="pt-4 text-body-1">
<v-text-field
v-model="store.userProfile.emergencyContact"
class="mb-3"
color="primary"
density="comfortable"
label="緊急聯絡人姓名"
variant="outlined"
/>
<v-text-field
v-model="store.userProfile.emergencyPhone"
color="primary"
density="comfortable"
label="緊急聯絡人電話"
variant="outlined"
/>
</v-card-text>
<v-card-actions class="justify-end px-4 pb-4">
<v-btn
class="rounded-lg px-4"
color="primary"
variant="flat"
@click="showEmergency = false"
>
儲存並關閉
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-app>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAppStore } from '@/stores/app'
const store = useAppStore()
const router = useRouter()
const route = useRoute()
const drawer = ref(false)
const activeTab = ref('home')
const showPrivacy = ref(false)
const showEmergency = ref(false)
// 無障礙狀態 (可持久化儲存於 localStorage)
const isLargeText = ref(localStorage.getItem('mode-large-text') === 'true')
watch(isLargeText, newVal => {
localStorage.setItem('mode-large-text', String(newVal))
})
// 同步底部導航狀態與當前路由
watch(
() => route.path,
path => {
if (path === '/') activeTab.value = 'home'
else if (path.startsWith('/self-assessment')) activeTab.value = 'self-assessment'
else if (path.startsWith('/appointment')) activeTab.value = 'appointment'
else if (path.startsWith('/checkin')) activeTab.value = 'checkin'
},
{ immediate: true },
)
function getBottomNavIconColor (value: string) {
return activeTab.value === value ? 'secondary' : 'secondary-darken-1'
}
function handleLogout () {
store.logout()
router.push('/login')
drawer.value = false
}
</script>
<style lang="scss">
// 行動優先的寬度限制,確保在大螢幕上也不會跑版
.max-width-mobile {
max-width: 600px;
margin: 0 auto;
}
// 大字體模式
.large-text-mode {
font-size: 1.15rem !important;
.v-list-item-title,
.v-btn,
.text-title-medium {
font-size: 1.25rem !important;
line-height: unset !important;
}
.v-card-text,
.text-body-1,
.text-body-2,
p {
font-size: 1.15rem !important;
line-height: 1.6 !important;
}
.text-h4,
.text-h5,
.text-h6 {
font-size: 1.5rem !important;
}
.bottom-nav-text {
font-size: 1rem;
}
}
.border-top {
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
</style>