Files
sports-empower-hub/app/citizen-frontend/src/pages/self-assessment.vue
T
skytek_xinliang 3fc6af0a85 fix: date select
2026-06-23 10:10:44 +08:00

514 lines
16 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>
<Layout>
<template #main>
<v-card class="mb-6" variant="flat">
<v-card-title class="text-primary">線上健康自評</v-card-title>
<v-card-subtitle class="text-grey-darken-3">
只需 3 分鐘,評估您的運動風險與健康狀態
</v-card-subtitle>
</v-card>
<!-- 進度指示器 -->
<v-row class="mb-6 justify-center">
<v-col v-for="(stepName, idx) in stepNames" :key="idx" class="text-center" cols="3">
<v-avatar
class="text-white font-weight-bold mb-1 elevation-1"
:color="
currentStep > idx ? 'primary' : currentStep === idx ? 'secondary' : 'grey-lighten-2'
"
size="28"
>
<v-icon v-if="currentStep > idx" icon="mdi-check" size="14" />
<span v-else class="text-title-small">{{ idx + 1 }}</span>
</v-avatar>
<div
class="text-title-small font-weight-bold"
:class="
currentStep === idx
? 'text-secondary'
: currentStep > idx
? 'text-primary'
: 'text-grey'
"
>
{{ stepName }}
</div>
</v-col>
</v-row>
<!-- 步驟內容區 -->
<v-card class="rounded-xl elevation-3 mb-6" color="surface">
<v-card-text class="py-2">
<!-- 步驟 1:基本資料 -->
<div v-if="currentStep === 0">
<h2 class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center">
<v-icon class="mr-2" color="secondary" icon="mdi-account-circle" />
基本資料確認
</h2>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
v-model="profileForm.name"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="姓名"
variant="outlined"
/>
</v-col>
<v-col cols="12" sm="6">
<v-select
v-model="profileForm.gender"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
:items="['男', '女']"
label="性別"
variant="outlined"
/>
</v-col>
<v-col cols="12" sm="6">
<v-date-input
v-model="birthdayDate"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="生日"
prepend-icon=""
prepend-inner-icon="mdi-calendar"
variant="outlined"
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model.number="profileForm.height"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="身高 (公分)"
type="number"
variant="outlined"
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model.number="profileForm.weight"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="體重 (公斤)"
type="number"
variant="outlined"
/>
</v-col>
</v-row>
<v-checkbox
v-model="profileForm.consent"
class="mt-2 text-caption"
color="primary"
density="compact"
hide-details
label="我已閱讀並同意個資授權條款"
/>
</div>
<!-- 步驟 2:活動量與自評 -->
<div v-if="currentStep === 1">
<h2 class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center">
<v-icon class="mr-2" color="secondary" icon="mdi-walk" />
活動習慣與想要改善的能力
</h2>
<p class="text-body-medium font-weight-bold text-grey-darken-2 mb-2">
1. 最近 7 天,您每週進行大運動幾次?
</p>
<v-radio-group v-model="assessmentForm.exerciseFrequency" class="mb-4" color="primary">
<v-card
class="mb-2 rounded-lg pa-1 cursor-pointer transition-card"
:color="assessmentForm.exerciseFrequency === '0' ? 'secondary' : 'grey-lighten-1'"
variant="outlined"
@click="assessmentForm.exerciseFrequency = '0'"
>
<v-radio label="0 次,幾乎沒有固定運動" value="0" />
</v-card>
<v-card
class="mb-2 rounded-lg pa-1 cursor-pointer transition-card"
:color="assessmentForm.exerciseFrequency === '1-2' ? 'secondary' : 'grey-lighten-1'"
variant="outlined"
@click="assessmentForm.exerciseFrequency = '1-2'"
>
<v-radio label="1-2 次,偶爾散步或伸展" value="1-2" />
</v-card>
<v-card
class="rounded-lg pa-1 cursor-pointer transition-card"
:color="assessmentForm.exerciseFrequency === '3+' ? 'secondary' : 'grey-lighten-1'"
variant="outlined"
@click="assessmentForm.exerciseFrequency = '3+'"
>
<v-radio label="3 次以上,有固定運動習慣" value="3+" />
</v-card>
</v-radio-group>
<p class="text-body-medium font-weight-bold text-grey-darken-2 mb-2">
2. 您目前最想改善的身體功能? (可複選)
</p>
<v-row>
<v-col v-for="imp in improvementsOptions" :key="imp" cols="6">
<v-card
class="rounded-lg py-2 px-1 text-center cursor-pointer transition-card"
:class="{ 'bg-teal-lighten-5': assessmentForm.wantedImprovements.includes(imp) }"
:color="
assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey-lighten-1'
"
variant="outlined"
@click="toggleImprovement(imp)"
>
<v-icon
class="mb-1"
:color="assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey'"
:icon="getImprovementIcon(imp)"
/>
<div class="text-title-large font-weight-bold">{{ imp }}</div>
</v-card>
</v-col>
</v-row>
</div>
<!-- 步驟 3:身體狀況自評 -->
<div v-if="currentStep === 2">
<h2 class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center">
<v-icon class="mr-2" color="secondary" icon="mdi-alert-decagram" />
身體限制與慢性病史
</h2>
<p class="text-body-medium font-weight-bold text-grey-darken-2 mb-2">
1. 過去一年是否有過跌倒史?
</p>
<v-radio-group v-model="assessmentForm.hasFalls" class="mb-4" color="primary" inline>
<v-radio label="無跌倒" :value="false" />
<v-radio label="有跌倒過" :value="true" />
</v-radio-group>
<p class="text-body-medium font-weight-bold text-grey-darken-2 mb-2">
2. 目前身體關節疼痛狀況?
</p>
<v-select
v-model="assessmentForm.painLevel"
class="mb-4"
color="primary"
density="comfortable"
:items="['無', '輕微', '中度', '重度']"
variant="outlined"
/>
<p class="text-body-medium font-weight-bold text-grey-darken-2 mb-2">
3. 您有哪些慢性疾病? (可複選)
</p>
<v-row class="mb-4">
<v-col v-for="disease in diseaseOptions" :key="disease" cols="6">
<v-checkbox
v-model="assessmentForm.chronicDiseases"
color="primary"
density="compact"
hide-details
:label="disease"
:value="disease"
/>
</v-col>
</v-row>
</div>
<!-- 步驟 4:自評結果 -->
<div v-if="currentStep === 3" class="text-center py-4">
<v-avatar class="elevation-2 mb-4" :color="getResultColor(resultLevel)" size="72">
<v-icon color="white" :icon="getResultIcon(resultLevel)" size="40" />
</v-avatar>
<h2
class="text-title-large font-weight-bold mb-1"
:class="`text-${getResultColor(resultLevel)}`"
>
自評分級:{{ resultLevel }}
</h2>
<p class="text-title-small text-grey-darken-2 mb-6">您的健康自評報告已產生</p>
<v-card class="rounded-xl pa-4 text-left mb-6" color="grey-lighten-4" variant="flat">
<div class="font-weight-bold mb-2 text-primary d-flex align-center">
<v-icon class="mr-1" icon="mdi-text-box-search" />
系統評估建議:
</div>
<p v-if="resultLevel === '低風險'" class="text-body-medium text-grey-darken-3 mb-2">
您的身體功能與日常活動習慣良好。建議您每週繼續保持 3
次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動處方。
</p>
<p
v-else-if="resultLevel === '需追蹤'"
class="text-body-medium text-grey-darken-3 mb-2"
>
由於您運動頻率偏低,或是過去有跌倒史,建議您可以前往運動中心預約專業普測,由現場教練為您提供適合的肌力與平衡回測建議。
</p>
<p v-else class="text-body-medium text-grey-darken-3 mb-2">
檢測到您有多項慢性病或中重度疼痛,運動時請特別留意。建議您先向您的家庭醫師或社區據點諮詢,並在運動普測現場合對您的健康狀態,以確保運動安全。
</p>
<div class="font-weight-bold text-body-medium text-secondary mt-3">
* 禁忌提醒:若收縮壓 >
160mmHg、有關節強烈疼痛或暈眩時,請立即暫停所有體能檢測與訓練!
</div>
</v-card>
</div>
</v-card-text>
<!-- 表單操作按鈕 -->
<v-divider />
<v-card-actions class="px-5 py-4 d-flex justify-between">
<v-btn
v-if="currentStep > 0 && currentStep < 3"
class="rounded-lg px-4"
color="primary"
variant="outlined"
@click="goToPreviousStep"
>
上一步
</v-btn>
<v-spacer />
<v-btn
v-if="currentStep < 2"
class="rounded-lg px-6"
color="primary"
:disabled="currentStep === 0 && !profileForm.consent"
variant="flat"
@click="goToNextStep"
>
下一步
</v-btn>
<v-btn
v-else-if="currentStep === 2"
class="rounded-lg px-6 text-white"
color="secondary"
variant="flat"
@click="handleSubmit"
>
看我的健康力分級
</v-btn>
<div v-else class="w-100 d-flex gap-2">
<v-btn
block
class="rounded-lg py-2"
color="primary"
to="/appointment"
variant="flat"
>
立即預約普測
</v-btn>
</div>
</v-card-actions>
</v-card>
<!-- 返回按鈕 (結果頁專用) -->
<v-btn
v-if="currentStep === 3"
block
class="rounded-lg py-2"
color="primary"
to="/"
variant="outlined"
>
回首頁健康儀表板
</v-btn>
</template>
</Layout>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import Layout from '@/components/layout/DefaultLayout.vue'
import { useAppStore } from '@/stores/app'
const store = useAppStore()
const currentStep = ref(0)
const stepNames = ['基本資料', '功能狀態', '健康限制', '自評結果']
const profileForm = reactive({
name: store.userProfile.name,
birthday: store.userProfile.birthday,
gender: store.userProfile.gender,
height: store.userProfile.height,
weight: store.userProfile.weight,
consent: store.userProfile.consent,
})
const assessmentForm = reactive({
exerciseFrequency: store.selfAssessment.exerciseFrequency,
wantedImprovements: [...store.selfAssessment.wantedImprovements],
hasFalls: store.selfAssessment.hasFalls,
painLevel: store.selfAssessment.painLevel,
chronicDiseases: [...store.selfAssessment.chronicDiseases],
})
const resultLevel = ref(store.selfAssessment.scoreLevel)
const improvementsOptions = ['肌力', '平衡', '心肺', '柔軟度', '敏捷', '體脂/BMI']
const diseaseOptions = ['高血壓', '糖尿病', '高血脂', '心臟病', '關節退化', '骨質疏鬆']
const birthdayDate = computed<Date | null>({
get: () => parseDateString(profileForm.birthday),
set: value => {
profileForm.birthday = formatDateString(value)
},
})
function parseDateString (value: string) {
const [year, month, day] = value.split('-').map(Number)
if (!year || !month || !day) {
return null
}
return new Date(year, month - 1, day)
}
function formatDateString (value: Date | null) {
if (!value) {
return ''
}
const year = value.getFullYear()
const month = String(value.getMonth() + 1).padStart(2, '0')
const day = String(value.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
function getImprovementIcon (imp: string) {
switch (imp) {
case '肌力': {
return 'mdi-arm-flex'
}
case '平衡': {
return 'mdi-scale-balance'
}
case '心肺': {
return 'mdi-heart-pulse'
}
case '柔軟度': {
return 'mdi-human-handsup'
}
case '敏捷': {
return 'mdi-run-fast'
}
case '體脂/BMI': {
return 'mdi-calculator'
}
default: {
return 'mdi-help-circle'
}
}
}
function toggleImprovement (imp: string) {
const index = assessmentForm.wantedImprovements.indexOf(imp)
if (index === -1) {
assessmentForm.wantedImprovements.push(imp)
} else {
assessmentForm.wantedImprovements.splice(index, 1)
}
}
function scrollToPageTop () {
window.scrollTo({ top: 0 })
}
function goToPreviousStep () {
currentStep.value -= 1
scrollToPageTop()
}
function goToNextStep () {
currentStep.value += 1
scrollToPageTop()
}
function handleSubmit () {
// 儲存至 store
store.updateProfile({
name: profileForm.name,
birthday: profileForm.birthday,
gender: profileForm.gender,
height: profileForm.height,
weight: profileForm.weight,
consent: profileForm.consent,
})
store.submitSelfAssessment({
exerciseFrequency: assessmentForm.exerciseFrequency,
wantedImprovements: assessmentForm.wantedImprovements,
hasFalls: assessmentForm.hasFalls,
painLevel: assessmentForm.painLevel,
chronicDiseases: assessmentForm.chronicDiseases,
})
// 取得計算後的結果
resultLevel.value = store.selfAssessment.scoreLevel
currentStep.value = 3
scrollToPageTop()
}
function getResultColor (lvl: string) {
if (lvl === '低風險') return 'success'
if (lvl === '需追蹤') return 'warning'
return 'error'
}
function getResultIcon (lvl: string) {
if (lvl === '低風險') return 'mdi-check-decagram'
if (lvl === '需追蹤') return 'mdi-alert-decagram'
return 'mdi-alert-octagon'
}
</script>
<style scoped>
.transition-card {
transition: all 0.25s ease;
border-width: 1.5px;
}
.transition-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
.gap-2 {
gap: 8px;
}
</style>