feat: draft
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<template #main>
|
||||
<div class="mb-6 text-center">
|
||||
<h1 class="text-h5 font-weight-black text-primary">公益普測與活動預約</h1>
|
||||
<p class="text-caption text-grey">報名免費體能普測,取得個人運動處方</p>
|
||||
</div>
|
||||
|
||||
<!-- 活動分類選擇 -->
|
||||
<v-tabs v-model="filterTab" class="mb-4 bg-white rounded-lg elevation-1" color="primary" grow>
|
||||
<v-tab value="all">全部活動</v-tab>
|
||||
<v-tab value="registered">我的報名</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- 活動列表 -->
|
||||
<div v-if="filteredAppointments.length === 0" class="text-center py-12">
|
||||
<v-icon class="mb-3" color="grey-lighten-1" icon="mdi-calendar-blank" size="64" />
|
||||
<div class="text-subtitle-1 text-grey font-weight-bold">目前沒有相關活動</div>
|
||||
</div>
|
||||
|
||||
<v-row v-else dense>
|
||||
<v-col v-for="apt in filteredAppointments" :key="apt.id" class="mb-4" cols="12">
|
||||
<v-card class="rounded-xl elevation-3 border-accent-light" color="surface">
|
||||
<!-- 卡片頂部狀態條 -->
|
||||
<v-card-item class="bg-teal-lighten-5 py-2">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<span class="text-caption font-weight-bold text-primary">
|
||||
<v-icon class="mr-1" icon="mdi-office-building" size="14" />
|
||||
{{ apt.organizer }}
|
||||
</span>
|
||||
|
||||
<v-chip
|
||||
class="font-weight-black text-caption"
|
||||
:color="getStatusColor(apt.status)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ apt.status }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text class="pt-4 px-4 pb-2">
|
||||
<h2 class="text-subtitle-1 font-weight-bold text-grey-darken-4 mb-3">
|
||||
{{ apt.title }}
|
||||
</h2>
|
||||
|
||||
<div class="d-flex align-start mb-2">
|
||||
<v-icon class="mr-2 mt-0.5" color="primary" icon="mdi-calendar-clock" size="18" />
|
||||
|
||||
<div class="text-body-2 text-grey-darken-3">
|
||||
<span class="font-weight-bold">{{ apt.date }}</span>
|
||||
<span class="ml-2 text-grey">{{ apt.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-start mb-2">
|
||||
<v-icon class="mr-2 mt-0.5" color="primary" icon="mdi-map-marker" size="18" />
|
||||
<div class="text-body-2 text-grey-darken-3">{{ apt.location }}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-start mb-3">
|
||||
<v-icon class="mr-2 mt-0.5" color="primary" icon="mdi-human-capacity" size="18" />
|
||||
|
||||
<div class="text-body-2 text-grey-darken-3">
|
||||
剩餘名額:<span class="font-weight-bold" :class="apt.slots < 10 ? 'text-error' : 'text-success'">{{ apt.slots }}</span> 位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<!-- 量測項目標籤 -->
|
||||
<div class="mb-2">
|
||||
<div class="text-caption font-weight-bold text-grey mb-1">量測項目:</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<v-chip
|
||||
v-for="item in apt.items"
|
||||
:key="item"
|
||||
class="font-weight-medium"
|
||||
color="primary"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ item }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- 卡片按鈕操作 -->
|
||||
<v-card-actions class="px-4 pb-4 pt-0">
|
||||
<!-- 未報名狀態 -->
|
||||
<template v-if="apt.status === '未報名'">
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg text-white font-weight-bold"
|
||||
color="secondary"
|
||||
:disabled="apt.slots === 0"
|
||||
variant="flat"
|
||||
@click="handleRegister(apt.id)"
|
||||
>
|
||||
{{ apt.slots === 0 ? '名額已滿' : '線上報名活動' }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<!-- 已報名狀態 -->
|
||||
<template v-else-if="apt.status === '已報名'">
|
||||
<v-row class="w-100" dense>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg text-white font-weight-bold"
|
||||
color="primary"
|
||||
prepend-icon="mdi-qrcode"
|
||||
variant="flat"
|
||||
@click="showCheckinQRCode(apt)"
|
||||
>
|
||||
出示 QR Code
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
@click="handleCancel(apt.id)"
|
||||
>
|
||||
取消報名
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<!-- 已報到狀態 -->
|
||||
<template v-else-if="apt.status === '已報到'">
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold"
|
||||
color="primary"
|
||||
to="/checkin"
|
||||
variant="outlined"
|
||||
>
|
||||
進行現場量測 (已報到)
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 報到 QR Code 對話框 -->
|
||||
<v-dialog v-model="qrcodeDialog" max-width="400">
|
||||
<v-card class="rounded-xl overflow-hidden text-center">
|
||||
<v-card-title class="bg-primary text-white font-weight-bold py-3">
|
||||
現場報到 QR Code
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<h3 class="text-subtitle-1 font-weight-bold mb-1 text-grey-darken-4">
|
||||
{{ selectedApt?.title }}
|
||||
</h3>
|
||||
|
||||
<p class="text-caption text-grey mb-4">請在現場合對處出示此條碼進行報到</p>
|
||||
|
||||
<!-- 模擬 QR Code SVG,更高級精緻 -->
|
||||
<div class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center">
|
||||
<svg class="qrcode-svg" height="180" viewBox="0 0 100 100" width="180">
|
||||
<!-- 四個角定位框 -->
|
||||
<path d="M 0,0 H 25 V 8 H 8 V 25 H 0 Z" fill="#1D7063" />
|
||||
<path d="M 75,0 H 100 V 25 H 92 V 8 H 75 Z" fill="#1D7063" />
|
||||
<path d="M 0,75 V 100 H 25 V 92 H 8 V 75 Z" fill="#1D7063" />
|
||||
<path d="M 92,75 V 92 H 75 V 100 H 100 V 75 Z" fill="#1D7063" />
|
||||
<!-- 內嵌二維碼方塊 -->
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="12"
|
||||
y="12"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="16"
|
||||
y="16"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="6"
|
||||
width="6"
|
||||
x="19"
|
||||
y="19"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="68"
|
||||
y="12"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="72"
|
||||
y="16"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="6"
|
||||
width="6"
|
||||
x="75"
|
||||
y="75"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="12"
|
||||
y="68"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="16"
|
||||
y="72"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="6"
|
||||
width="6"
|
||||
x="19"
|
||||
y="75"
|
||||
/>
|
||||
|
||||
<!-- 中間隨機小方塊 -->
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="8"
|
||||
width="8"
|
||||
x="40"
|
||||
y="20"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="14"
|
||||
width="6"
|
||||
x="52"
|
||||
y="12"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="6"
|
||||
width="16"
|
||||
x="42"
|
||||
y="36"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#47C2A6"
|
||||
height="10"
|
||||
width="28"
|
||||
x="36"
|
||||
y="48"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="12"
|
||||
width="14"
|
||||
x="68"
|
||||
y="42"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="16"
|
||||
width="16"
|
||||
x="72"
|
||||
y="60"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="12"
|
||||
width="18"
|
||||
x="44"
|
||||
y="68"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#47C2A6"
|
||||
height="6"
|
||||
width="24"
|
||||
x="40"
|
||||
y="82"
|
||||
/>
|
||||
</svg>
|
||||
<!-- 紅色掃描雷射線 -->
|
||||
<div class="laser-line" />
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey-darken-2 font-weight-bold mb-4">
|
||||
姓名: {{ store.userProfile.name }} 先生 | 電話: {{ store.userProfile.phone }}
|
||||
</div>
|
||||
|
||||
<!-- 模擬現場合對處,高齡測試快捷鍵 -->
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold"
|
||||
color="success"
|
||||
prepend-icon="mdi-qrcode-scan"
|
||||
variant="tonal"
|
||||
@click="simulateOnSiteCheckin"
|
||||
>
|
||||
模擬現場人員掃描 (立刻報到)
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4 justify-center">
|
||||
<v-btn class="rounded-lg px-6" color="primary" variant="flat" @click="qrcodeDialog = false">
|
||||
關閉條碼
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { type Appointment, useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
const filterTab = ref('all')
|
||||
|
||||
const qrcodeDialog = ref(false)
|
||||
const selectedApt = ref<Appointment | null>(null)
|
||||
|
||||
const filteredAppointments = computed(() => {
|
||||
if (filterTab.value === 'registered') {
|
||||
return store.appointments.filter(a => a.status === '已報名' || a.status === '已報到')
|
||||
}
|
||||
return store.appointments
|
||||
})
|
||||
|
||||
function getStatusColor (status: string) {
|
||||
switch (status) {
|
||||
case '未報名': { return 'grey-darken-1'
|
||||
}
|
||||
case '已報名': { return 'secondary'
|
||||
}
|
||||
case '已報到': { return 'success'
|
||||
}
|
||||
default: { return 'grey'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRegister (id: string) {
|
||||
store.registerAppointment(id)
|
||||
}
|
||||
|
||||
function handleCancel (id: string) {
|
||||
store.cancelAppointment(id)
|
||||
}
|
||||
|
||||
function showCheckinQRCode (apt: Appointment) {
|
||||
selectedApt.value = apt
|
||||
qrcodeDialog.value = true
|
||||
}
|
||||
|
||||
function simulateOnSiteCheckin () {
|
||||
if (selectedApt.value) {
|
||||
store.checkinAppointment(selectedApt.value.id)
|
||||
qrcodeDialog.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gap-1 {
|
||||
gap: 4px;
|
||||
}
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.qrcode-wrapper {
|
||||
position: relative;
|
||||
width: 204px;
|
||||
height: 204px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qrcode-svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 掃描雷射線效果 */
|
||||
.laser-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: #ff3b30;
|
||||
box-shadow: 0 0 8px #ff3b30;
|
||||
animation: scan 2.5s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { top: 0%; }
|
||||
50% { top: 100%; }
|
||||
100% { top: 0%; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<template #main>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="text-h5 font-weight-black text-primary">現場量測與報到</h1>
|
||||
<p class="text-caption text-grey">在各量測站出示此頁面,由現場教練或志工登錄數據</p>
|
||||
</div>
|
||||
|
||||
<!-- 報到活動資訊 -->
|
||||
<v-card v-if="activeAppointment" class="rounded-xl elevation-3 border-accent-light mb-6" color="surface">
|
||||
<v-card-text class="pa-4">
|
||||
<div class="d-flex align-center justify-between mb-3">
|
||||
<span class="text-subtitle-2 font-weight-bold text-primary">
|
||||
<v-icon class="mr-1" icon="mdi-map-marker-radius" />
|
||||
現場檢測站:{{ activeAppointment.location.split(' ')[0] }}
|
||||
</span>
|
||||
|
||||
<v-chip class="font-weight-black" color="success" size="small" variant="flat">
|
||||
已報到
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<h2 class="text-subtitle-1 font-weight-black text-grey-darken-4 mb-2">
|
||||
{{ activeAppointment.title }}
|
||||
</h2>
|
||||
|
||||
<div class="text-caption text-grey">
|
||||
主辦單位:{{ activeAppointment.organizer }} | 日期:{{ activeAppointment.date }}
|
||||
</div>
|
||||
|
||||
<!-- 量測總進度條 -->
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-space-between align-center mb-1">
|
||||
<span class="text-caption font-weight-bold text-grey-darken-2">測量站點完成度</span>
|
||||
<span class="text-caption font-weight-bold text-primary">{{ completedCount }} / {{ totalCount }} ({{ progressPercent }}%)</span>
|
||||
</div>
|
||||
|
||||
<v-progress-linear
|
||||
v-model="progressPercent"
|
||||
class="elevation-1"
|
||||
color="accent"
|
||||
height="10"
|
||||
rounded
|
||||
striped
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 未報到提示 -->
|
||||
<v-card v-else class="rounded-xl elevation-3 pa-6 text-center mb-6" color="surface">
|
||||
<v-icon class="mb-3" color="warning" icon="mdi-qrcode-scan" size="64" />
|
||||
<h3 class="text-subtitle-1 font-weight-bold mb-2">尚未在活動現場合對報到</h3>
|
||||
<p class="text-body-2 text-grey mb-4">請先前往「活動預約」頁面,出示您的報到條碼給現場志工進行核對。</p>
|
||||
<v-btn class="rounded-lg" color="primary" to="/appointment">前往查看預約活動</v-btn>
|
||||
</v-card>
|
||||
|
||||
<!-- 快捷測試功能 (對 Demo 非常友善) -->
|
||||
<v-card v-if="activeAppointment && completedCount < totalCount" class="rounded-xl mb-6 border-accent-light bg-teal-lighten-5" color="primary" variant="outlined">
|
||||
<v-card-text class="pa-4 d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-subtitle-2 font-weight-bold text-primary">
|
||||
<v-icon class="mr-1" icon="mdi-heart-cog" />
|
||||
高齡友善測試輔助
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey-darken-1">一鍵填入所有剩餘關卡數值,便於檢視報告</div>
|
||||
</div>
|
||||
|
||||
<v-btn class="rounded-lg font-weight-bold text-white" color="secondary" size="small" @click="simulateAllMeasurements">
|
||||
一鍵模擬完成
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 11 個量測關卡列表 -->
|
||||
<div v-if="activeAppointment" class="mb-6">
|
||||
<h3 class="text-subtitle-2 font-weight-black text-primary mb-3">關卡列表</h3>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="(station, key) in store.measurements" :key="key" class="mb-2" cols="12">
|
||||
<v-card
|
||||
class="rounded-xl elevation-2 cursor-pointer transition-card"
|
||||
:class="{ 'border-success': station.completed, 'border-grey': !station.completed }"
|
||||
:to="`/checkin/${key}`"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between pa-4">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
class="mr-3 text-white"
|
||||
:color="station.completed ? 'success' : 'grey-lighten-2'"
|
||||
size="40"
|
||||
>
|
||||
<v-icon :icon="station.completed ? 'mdi-check' : getStationIcon(key)" size="20" />
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-body-1" :class="station.completed ? 'text-success' : 'text-grey-darken-4'">
|
||||
{{ station.name }}
|
||||
</div>
|
||||
<!-- 已完成,顯示數值 -->
|
||||
<div v-if="station.completed" class="text-caption text-grey-darken-2">
|
||||
{{ getMeasurementSummary(key, station.values) }}
|
||||
<span class="ml-2 text-grey-lighten-1">| {{ station.time?.split(' ')[1] }}</span>
|
||||
</div>
|
||||
<!-- 未完成,顯示待測 -->
|
||||
<div v-else class="text-caption text-grey-darken-1">
|
||||
等待現場檢測員登錄
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<v-icon color="grey-lighten-1" icon="mdi-chevron-right" />
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- 底部產出報告按鈕 (當全部完成時顯示) -->
|
||||
<v-btn
|
||||
v-if="activeAppointment"
|
||||
block
|
||||
class="rounded-xl font-weight-bold text-white elevation-4 py-4 d-flex align-center justify-center mb-8"
|
||||
:color="completedCount === totalCount ? 'secondary' : 'grey-lighten-1'"
|
||||
:disabled="completedCount < totalCount"
|
||||
size="large"
|
||||
@click="generateReport"
|
||||
>
|
||||
<v-icon class="mr-2" icon="mdi-file-chart-outline" size="24" />
|
||||
產出個人健康力報告
|
||||
</v-btn>
|
||||
|
||||
<!-- 報告生成成功彈窗 -->
|
||||
<v-dialog v-model="reportSuccessDialog" max-width="400" persistent>
|
||||
<v-card class="rounded-xl overflow-hidden text-center pa-6">
|
||||
<v-avatar class="mx-auto mb-4 elevation-3" color="success" size="72">
|
||||
<v-icon color="white" icon="mdi-check-all" size="40" />
|
||||
</v-avatar>
|
||||
|
||||
<h2 class="text-h5 font-weight-black text-success mb-2">報告計算完成!</h2>
|
||||
|
||||
<p class="text-body-2 text-grey-darken-2 mb-6">
|
||||
您的健康力五維雷達圖、與同齡區間的參考值以及健康力總分已成功計算更新。
|
||||
</p>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg py-2 font-weight-bold"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
@click="goToDashboard"
|
||||
>
|
||||
看我的健康力儀表板
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
const router = useRouter()
|
||||
const reportSuccessDialog = ref(false)
|
||||
|
||||
// 尋找已報到的活動
|
||||
const activeAppointment = computed(() => {
|
||||
return store.appointments.find(a => a.status === '已報到')
|
||||
})
|
||||
|
||||
const completedCount = computed(() => store.completedStationsCount)
|
||||
const totalCount = computed(() => store.totalStationsCount)
|
||||
const progressPercent = computed(() => store.progressPercent)
|
||||
|
||||
function getStationIcon (code: string) {
|
||||
switch (code) {
|
||||
case 'BloodPressure': { return 'mdi-heart-flash'
|
||||
}
|
||||
case 'BodyFat': { return 'mdi-scale-bathroom'
|
||||
}
|
||||
case 'GripStrength': { return 'mdi-hand-back-left'
|
||||
}
|
||||
case 'Sit5x': { return 'mdi-human-male-height'
|
||||
}
|
||||
case 'Sit30s': { return 'mdi-chair-school'
|
||||
}
|
||||
case 'StepTest': { return 'mdi-run-fast'
|
||||
}
|
||||
case 'ArmTest': { return 'mdi-arm-flex'
|
||||
}
|
||||
case 'BackTest': { return 'mdi-human-stretching'
|
||||
}
|
||||
case 'SitReachTest': { return 'mdi-human-stretching'
|
||||
}
|
||||
case 'UpGo8ft': { return 'mdi-navigation'
|
||||
}
|
||||
case 'OneLegTest': { return 'mdi-scale-balance'
|
||||
}
|
||||
default: { return 'mdi-checkbox-blank-circle-outline'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMeasurementSummary (code: string, vals: Record<string, any>) {
|
||||
switch (code) {
|
||||
case 'BloodPressure': {
|
||||
return `收縮壓: ${vals.systolic} / 舒張壓: ${vals.diastolic} mmHg | 心率: ${vals.heartRate} bpm`
|
||||
}
|
||||
case 'BodyFat': {
|
||||
return `BMI: ${vals.bmi} | 體脂率: ${vals.bodyFatPct}%`
|
||||
}
|
||||
case 'GripStrength': {
|
||||
return `左手: ${vals.leftHand} kg | 右手: ${vals.rightHand} kg`
|
||||
}
|
||||
case 'Sit5x': {
|
||||
return `耗時: ${vals.durationSeconds} 秒`
|
||||
}
|
||||
case 'Sit30s': {
|
||||
return `次數: ${vals.count} 次`
|
||||
}
|
||||
case 'StepTest': {
|
||||
return `踏步: ${vals.count} 次`
|
||||
}
|
||||
case 'ArmTest': {
|
||||
return `屈舉: ${vals.count} 次`
|
||||
}
|
||||
case 'BackTest': {
|
||||
return `左手: ${vals.leftDistanceCm} cm | 右手: ${vals.rightDistanceCm} cm`
|
||||
}
|
||||
case 'SitReachTest': {
|
||||
return `伸展: ${vals.distanceCm} cm`
|
||||
}
|
||||
case 'UpGo8ft': {
|
||||
return `耗時: ${vals.durationSeconds} 秒`
|
||||
}
|
||||
case 'OneLegTest': {
|
||||
return `左腳: ${vals.leftSeconds} 秒 | 右腳: ${vals.rightSeconds} 秒`
|
||||
}
|
||||
default: {
|
||||
return '數據登錄成功'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模擬一鍵完成所有測量 (對 Demo 极為有用)
|
||||
function simulateAllMeasurements () {
|
||||
// 自動將剩餘的 9 站填入合理健康的隨機值
|
||||
const m = store.measurements
|
||||
|
||||
if (!m.GripStrength.completed) {
|
||||
store.saveMeasurement('GripStrength', { leftHand: 32, rightHand: 34 })
|
||||
}
|
||||
if (!m.Sit5x.completed) {
|
||||
store.saveMeasurement('Sit5x', { durationSeconds: 8.5 })
|
||||
}
|
||||
if (!m.Sit30s.completed) {
|
||||
store.saveMeasurement('Sit30s', { count: 18 })
|
||||
}
|
||||
if (!m.StepTest.completed) {
|
||||
store.saveMeasurement('StepTest', { count: 92 })
|
||||
}
|
||||
if (!m.ArmTest.completed) {
|
||||
store.saveMeasurement('ArmTest', { count: 20 })
|
||||
}
|
||||
if (!m.BackTest.completed) {
|
||||
store.saveMeasurement('BackTest', { leftDistanceCm: 2, rightDistanceCm: 3 })
|
||||
}
|
||||
if (!m.SitReachTest.completed) {
|
||||
store.saveMeasurement('SitReachTest', { distanceCm: 14 })
|
||||
}
|
||||
if (!m.UpGo8ft.completed) {
|
||||
store.saveMeasurement('UpGo8ft', { durationSeconds: 5.8 })
|
||||
}
|
||||
if (!m.OneLegTest.completed) {
|
||||
store.saveMeasurement('OneLegTest', { leftSeconds: 24, rightSeconds: 26 })
|
||||
}
|
||||
}
|
||||
|
||||
function generateReport () {
|
||||
store.generateFinalReport()
|
||||
reportSuccessDialog.value = true
|
||||
}
|
||||
|
||||
function goToDashboard () {
|
||||
reportSuccessDialog.value = false
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.transition-card {
|
||||
transition: all 0.2s ease;
|
||||
border-width: 1.5px !important;
|
||||
}
|
||||
|
||||
.transition-card:hover {
|
||||
transform: translateY(-1.5px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.border-success {
|
||||
border-color: rgba(76, 175, 80, 0.5) !important;
|
||||
}
|
||||
|
||||
.border-grey {
|
||||
border-color: rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.border-accent-light {
|
||||
border: 1px solid rgba(71, 194, 166, 0.2) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<template #main>
|
||||
<!-- 頂部導航 -->
|
||||
<v-btn
|
||||
class="mb-4 font-weight-bold px-0"
|
||||
color="primary"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
to="/checkin"
|
||||
variant="text"
|
||||
>
|
||||
返回量測站清單
|
||||
</v-btn>
|
||||
|
||||
<!-- 關卡詳細卡片 -->
|
||||
<v-card
|
||||
v-if="station"
|
||||
class="rounded-xl elevation-3 border-accent-light mb-6"
|
||||
color="surface"
|
||||
>
|
||||
<v-card-item class="bg-primary text-white py-4">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 text-white elevation-1" color="accent" size="48">
|
||||
<v-icon :icon="station.icon" size="24" />
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<h1 class="text-h6 font-weight-black">{{ station.name }}</h1>
|
||||
|
||||
<span class="text-caption text-white-70"
|
||||
>第 {{ currentStationIndex }} 站 / 共 11 站</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text class="pa-5">
|
||||
<!-- 關卡簡介 -->
|
||||
<div class="bg-teal-lighten-5 rounded-lg pa-4 mb-6 text-body-2 text-primary-darken-1">
|
||||
<div class="font-weight-bold mb-1 d-flex align-center">
|
||||
<v-icon class="mr-1" icon="mdi-information-outline" size="18" />
|
||||
檢測說明:
|
||||
</div>
|
||||
{{ station.description }}
|
||||
</div>
|
||||
|
||||
<!-- 數據輸入表單 -->
|
||||
<v-form @submit.prevent="handleSave">
|
||||
<h3 class="text-subtitle-2 font-weight-bold text-grey-darken-3 mb-3">數據錄入:</h3>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="field in station.fields" :key="field.key" class="mb-4" cols="12">
|
||||
<div class="text-body-2 font-weight-bold text-grey-darken-3 mb-1">
|
||||
{{ field.label }} <span class="text-caption text-grey">({{ field.unit }})</span>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
v-model.number="formValues[field.key]"
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
:placeholder="field.placeholder"
|
||||
required
|
||||
rounded="lg"
|
||||
:rules="[(v) => (v !== null && v !== undefined) || '此欄位不可為空']"
|
||||
step="any"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold text-white elevation-2 mt-2"
|
||||
color="secondary"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
儲存並進入下一站
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 參考值與說明卡片 -->
|
||||
<v-card v-if="station" class="rounded-xl elevation-2 pa-4" color="surface">
|
||||
<div class="d-flex align-center mb-3 text-primary font-weight-bold text-subtitle-2">
|
||||
<v-icon class="mr-1" icon="mdi-heart-plus-outline" />
|
||||
65 歲以上高齡正常參考標準
|
||||
</div>
|
||||
|
||||
<v-divider class="mb-3" />
|
||||
|
||||
<div class="text-body-2 text-grey-darken-3 whitespace-pre-line">
|
||||
{{ station.reference }}
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
|
||||
const code = computed(() => route.params.code as string)
|
||||
|
||||
// 各個測量站點的詳細規格定義
|
||||
const stationsSpec: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
icon: string
|
||||
description: string
|
||||
reference: string
|
||||
fields: Array<{
|
||||
key: string
|
||||
label: string
|
||||
unit: string
|
||||
placeholder: string
|
||||
defaultValue: number
|
||||
}>
|
||||
}
|
||||
> = {
|
||||
BloodPressure: {
|
||||
name: '血壓與心率量測',
|
||||
icon: 'mdi-heart-flash',
|
||||
description:
|
||||
'請在放鬆狀態下靜坐 5 分鐘,手臂平放於桌面上,將袖帶綁在手肘上方兩公分處,按下測量鈕。檢測員請錄入血壓儀數值。',
|
||||
reference:
|
||||
'• 收縮壓 (高壓): 120-139 mmHg (正常偏高),超過 140 mmHg 為高血壓警告。\n• 舒張壓 (低壓): 80-89 mmHg,超過 90 mmHg 為高血壓警告。\n• 靜止心率: 60-100 bpm。',
|
||||
fields: [
|
||||
{
|
||||
key: 'systolic',
|
||||
label: '收縮壓 (高壓)',
|
||||
unit: 'mmHg',
|
||||
placeholder: '如: 125',
|
||||
defaultValue: 120,
|
||||
},
|
||||
{
|
||||
key: 'diastolic',
|
||||
label: '舒張壓 (低壓)',
|
||||
unit: 'mmHg',
|
||||
placeholder: '如: 80',
|
||||
defaultValue: 80,
|
||||
},
|
||||
{ key: 'heartRate', label: '安靜心率', unit: 'bpm', placeholder: '如: 72', defaultValue: 70 },
|
||||
],
|
||||
},
|
||||
BodyFat: {
|
||||
name: '體組成分析 (BMI & 體脂)',
|
||||
icon: 'mdi-scale-bathroom',
|
||||
description: '請脫鞋站在體脂計上,雙手握緊金屬把手。本站測量體重、BMI 與體脂肪率。',
|
||||
reference:
|
||||
'• 高齡者 BMI 正常區間: 20.0 - 26.9 (高齡適度豐腴有助於功能儲備)。\n• 體脂肪率正常區間:\n - 男性: 15% - 25% (超過 25% 為肥胖)\n - 女性: 20% - 30% (超過 30% 為肥胖)',
|
||||
fields: [
|
||||
{
|
||||
key: 'bmi',
|
||||
label: 'BMI 身體質量指數',
|
||||
unit: 'kg/m²',
|
||||
placeholder: '如: 22.5',
|
||||
defaultValue: 22,
|
||||
},
|
||||
{
|
||||
key: 'bodyFatPct',
|
||||
label: '體脂肪率',
|
||||
unit: '%',
|
||||
placeholder: '如: 24.0',
|
||||
defaultValue: 25,
|
||||
},
|
||||
],
|
||||
},
|
||||
GripStrength: {
|
||||
name: '左右手握力測驗 (肌力)',
|
||||
icon: 'mdi-hand-back-left',
|
||||
description:
|
||||
'受測者自然站立,手持握力器呈 90 度或自然下垂,全力緊握。左右手各測兩次,取最大值錄入。評估上肢最大肌力。',
|
||||
reference:
|
||||
'• 高齡男性正常標準: 大於 28 公斤。\n• 高齡女性正常標準: 大於 18 公斤。\n• 數值過低可能代表肌少症前期,需加強抗阻力運動。',
|
||||
fields: [
|
||||
{
|
||||
key: 'leftHand',
|
||||
label: '左手最大握力',
|
||||
unit: 'kg',
|
||||
placeholder: '如: 30',
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
key: 'rightHand',
|
||||
label: '右手最大握力',
|
||||
unit: 'kg',
|
||||
placeholder: '如: 32',
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
Sit5x: {
|
||||
name: '5次坐站測試 (下肢肌力)',
|
||||
icon: 'mdi-human-male-height',
|
||||
description:
|
||||
'受測者雙手抱胸,坐在無扶手椅子上。聽到口令後,以最快速度完成「站起-坐下」5 次。記錄完成的秒數。',
|
||||
reference:
|
||||
'• 65 歲以上長者正常標準: 小於 12 秒完成。\n• 若超過 15 秒,表示下肢肌力明顯不足,跌倒風險增高,建議加入股四頭肌訓練。',
|
||||
fields: [
|
||||
{
|
||||
key: 'durationSeconds',
|
||||
label: '5次坐站完成時間',
|
||||
unit: '秒',
|
||||
placeholder: '如: 9.8',
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
Sit30s: {
|
||||
name: '30秒椅子坐立 (肌肉耐力)',
|
||||
icon: 'mdi-chair-school',
|
||||
description:
|
||||
'雙手抱胸交叉於肩前,坐在高度約 43 公分的椅子中間。聽到開始口令後,30 秒內全力完成「完全站立且直立-坐回椅子」的次數。',
|
||||
reference:
|
||||
'• 65-69 歲正常標準:\n - 男性: 12 - 18 次\n - 女性: 11 - 17 次\n• 此項目量測長者下肢肌肉耐力,是日常行走和樓梯攀爬的基礎。',
|
||||
fields: [
|
||||
{ key: 'count', label: '30秒內坐立次數', unit: '次', placeholder: '如: 14', defaultValue: 0 },
|
||||
],
|
||||
},
|
||||
StepTest: {
|
||||
name: '2分鐘原地抬膝踏步 (心肺耐力)',
|
||||
icon: 'mdi-run-fast',
|
||||
description:
|
||||
'量測大轉子與髕骨中點高度,在牆上做記號。受測者聽到開始後,原地踏步,大腿必須抬高至記號高度。計算 2 分鐘內「右膝」抬起的次數。',
|
||||
reference:
|
||||
'• 65-69 歲正常標準:\n - 男性: 86 - 116 次\n - 女性: 73 - 107 次\n• 評估長者有氧心肺適能,是維持長途行走體力的指標。',
|
||||
fields: [
|
||||
{
|
||||
key: 'count',
|
||||
label: '2分鐘右膝抬起次數',
|
||||
unit: '次',
|
||||
placeholder: '如: 80',
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
ArmTest: {
|
||||
name: '30秒肱二頭肌手臂屈舉 (上肢肌耐力)',
|
||||
icon: 'mdi-arm-flex',
|
||||
description:
|
||||
'坐在椅子上,優勢手持啞鈴 (男性 8 磅 / 女性 5 磅) 自然下垂。聽到開始後,30 秒內以最快速度完成前臂彎舉與放下的次數。',
|
||||
reference:
|
||||
'• 65-69 歲正常標準:\n - 男性 (8磅): 15 - 21 次\n - 女性 (5磅): 13 - 19 次\n• 評估上肢日常拿取重物、做家事所需的肌肉耐力。',
|
||||
fields: [
|
||||
{
|
||||
key: 'count',
|
||||
label: '30秒手臂屈舉次數',
|
||||
unit: '次',
|
||||
placeholder: '如: 16',
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
BackTest: {
|
||||
name: '抓背測驗 (上肢關節柔軟度)',
|
||||
icon: 'mdi-human-stretching',
|
||||
description:
|
||||
'一隻手從肩上向後下方伸,另一隻手從腰部往後上方伸,雙手中指儘量靠近並重疊。量測兩中指間距離 (重疊為正,未觸碰為負)。',
|
||||
reference:
|
||||
'• 65-69 歲正常標準:\n - 男性: -16 至 -2 公分\n - 女性: -8 至 +3 公分\n• 用於評估肩膀關節的靈活性,攸關穿衣、梳頭等日常自理動作。',
|
||||
fields: [
|
||||
{
|
||||
key: 'leftDistanceCm',
|
||||
label: '左手在上時兩指間距 (可為負值)',
|
||||
unit: '公分',
|
||||
placeholder: '如: -5 或 2',
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
key: 'rightDistanceCm',
|
||||
label: '右手在上時兩指間距 (可為負值)',
|
||||
unit: '公分',
|
||||
placeholder: '如: -3 或 4',
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
SitReachTest: {
|
||||
name: '坐姿體前彎 (下肢與腰部柔軟度)',
|
||||
icon: 'mdi-human-stretching',
|
||||
description:
|
||||
'坐在椅子邊緣,一腳伸直,腳尖勾起,另一腳彎曲平放。吸氣後呼氣,雙手重疊慢慢向前伸觸碰腳尖。測量中指端與腳尖距離。',
|
||||
reference:
|
||||
'• 65-69 歲正常標準:\n - 男性: -8 至 +8 公分\n - 女性: -3 至 +12 公分\n• 用於評估大腿後側肌群與腰部的柔軟度,柔軟度佳可有效減緩腰酸背痛並預防拉傷。',
|
||||
fields: [
|
||||
{
|
||||
key: 'distanceCm',
|
||||
label: '前彎中指與腳尖距離',
|
||||
unit: '公分',
|
||||
placeholder: '如: 5 (超過腳尖為正)',
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
UpGo8ft: {
|
||||
name: '2.44公尺起立繞物 (動態平衡與敏捷)',
|
||||
icon: 'mdi-navigation',
|
||||
description:
|
||||
'坐在椅子上,前方 2.44 公尺 (8英尺) 處放置角錐。聽到開始口令後,起身快步繞過角錐並走回椅子坐下。記錄完成秒數。',
|
||||
reference:
|
||||
'• 65 歲以上長者正常標準: 小於 8.5 秒完成。\n• 若大於 9.0 秒,代表敏捷度與動態平衡功能衰退,日常過馬路或避開障礙物時跌倒風險顯著升高。',
|
||||
fields: [
|
||||
{
|
||||
key: 'durationSeconds',
|
||||
label: '起立繞物完成時間',
|
||||
unit: '秒',
|
||||
placeholder: '如: 6.2',
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
OneLegTest: {
|
||||
name: '開眼單足立 (靜態平衡力)',
|
||||
icon: 'mdi-scale-balance',
|
||||
description:
|
||||
'受測者雙手叉腰,一腳抬離地面,開始計時。當支撐腳移動、雙手離開腰部或懸空腳觸地時停止。左右腳各測一次,取最大秒數。',
|
||||
reference:
|
||||
'• 65 歲以上正常標準: 大於 15 秒。\n• 低於 10 秒代表靜態平衡力嚴重退化,在日常站立、穿褲子或滑倒時缺乏瞬間反應力。',
|
||||
fields: [
|
||||
{
|
||||
key: 'leftSeconds',
|
||||
label: '左腳站立時間',
|
||||
unit: '秒',
|
||||
placeholder: '如: 18',
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
key: 'rightSeconds',
|
||||
label: '右腳站立時間',
|
||||
unit: '秒',
|
||||
placeholder: '如: 20',
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const station = computed(() => stationsSpec[code.value])
|
||||
|
||||
// 計算當前關卡在 11 關中的順序
|
||||
const currentStationIndex = computed(() => {
|
||||
const keys = Object.keys(stationsSpec)
|
||||
return keys.indexOf(code.value) + 1
|
||||
})
|
||||
|
||||
// 表單綁定的動態狀態
|
||||
const formValues = reactive<Record<string, any>>({})
|
||||
|
||||
// 監聽 code 改變,初始化表單預設值
|
||||
watch(
|
||||
code,
|
||||
(newCode) => {
|
||||
if (stationsSpec[newCode]) {
|
||||
// 優先從 store 中讀取已有的測量數據
|
||||
const existing = store.measurements[newCode]
|
||||
for (const field of stationsSpec[newCode].fields) {
|
||||
formValues[field.key] =
|
||||
existing && existing.completed && existing.values[field.key] !== undefined
|
||||
? existing.values[field.key]
|
||||
: field.defaultValue
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function handleSave() {
|
||||
// 將數據儲存到 store
|
||||
store.saveMeasurement(code.value, { ...formValues })
|
||||
|
||||
// 尋找下一站
|
||||
const keys = Object.keys(stationsSpec)
|
||||
const currentIndex = keys.indexOf(code.value)
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < keys.length - 1) {
|
||||
// 導向下一站
|
||||
const nextCode = keys[currentIndex + 1]
|
||||
router.push(`/checkin/${nextCode}`)
|
||||
} else {
|
||||
// 已完成最後一站,返回量測清單
|
||||
router.push('/checkin')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.border-accent-light {
|
||||
border: 1px solid rgba(71, 194, 166, 0.2) !important;
|
||||
}
|
||||
|
||||
.whitespace-pre-line {
|
||||
white-space: pre-line;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,711 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<template #main>
|
||||
<template v-if="store.isLoggedIn">
|
||||
<!-- 頂部:我的健康力護照卡片 (Taipei Masters Hub) -->
|
||||
<v-card
|
||||
class="passport-card rounded-xl text-white pa-5 elevation-8 mb-6 position-relative overflow-hidden"
|
||||
>
|
||||
<!-- 背景裝飾圓圈 -->
|
||||
<div class="bg-circle-1" />
|
||||
<div class="bg-circle-2" />
|
||||
|
||||
<div class="d-flex justify-space-between align-center mb-6 z-index-1 position-relative">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 border-white" color="accent" size="44">
|
||||
<span class="text-white font-weight-black text-subtitle-1">{{
|
||||
store.userProfile.name.substring(1)
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<h2 class="text-subtitle-1 font-weight-black mb-0">
|
||||
{{ store.userProfile.name }} 先生
|
||||
</h2>
|
||||
|
||||
<span class="text-caption text-white-70">更新於今日 {{ todayTimeString }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 健康力分數大圓盤 -->
|
||||
<div class="score-disk d-flex flex-column justify-center align-center">
|
||||
<span class="score-val">{{ store.healthScore }}</span>
|
||||
<span class="score-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能儲備與運動處方簡介 -->
|
||||
<div class="z-index-1 position-relative mb-4">
|
||||
<div class="d-inline-flex align-center bg-white-20 px-3 py-1 rounded-pill mb-3">
|
||||
<v-icon class="mr-1 text-accent-light" icon="mdi-heart-pulse" size="16" />
|
||||
<span class="text-caption font-weight-bold">功能儲備:{{ functionalReserve }}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-body-2 font-weight-medium bg-white-10 rounded-lg pa-3">
|
||||
<div class="font-weight-black mb-1 text-accent-light d-flex align-center">
|
||||
<v-icon class="mr-1" icon="mdi-notebook-check-outline" size="16" />
|
||||
本週處方建議:
|
||||
</div>
|
||||
{{ currentWeeklyPrescription }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="bg-white-20 my-3 z-index-1 position-relative" />
|
||||
|
||||
<!-- 四項核心能力數值簡覽 -->
|
||||
<v-row class="z-index-1 position-relative text-center" dense>
|
||||
<v-col cols="3">
|
||||
<div class="text-caption text-white-70">肌力</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-black">
|
||||
{{ store.radarData.strength }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="3">
|
||||
<div class="text-caption text-white-70">平衡</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-black">
|
||||
{{ store.radarData.balance }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="3">
|
||||
<div class="text-caption text-white-70">敏捷</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-black">
|
||||
{{ store.radarData.agility }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="3">
|
||||
<div class="text-caption text-white-70">心肺</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-black">
|
||||
{{ store.radarData.cardio }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
||||
<!-- 中間:功能指標與五維雷達圖 -->
|
||||
<v-card class="rounded-xl elevation-3 border-accent-light mb-6 pa-4" color="surface">
|
||||
<h3 class="text-subtitle-1 font-weight-black text-primary mb-2 d-flex align-center">
|
||||
<v-icon class="mr-2" color="secondary" icon="mdi-radar" />
|
||||
個人五維健康力指標
|
||||
</h3>
|
||||
|
||||
<p class="text-caption text-grey mb-4">
|
||||
綜合 11 項現場普測數值計算得出,藍色為同齡正常參考值區間。
|
||||
</p>
|
||||
|
||||
<!-- 純 SVG 雷達圖,不依賴任何外部圖表庫,保證完美渲染 -->
|
||||
<div class="d-flex justify-center my-2 position-relative">
|
||||
<svg class="radar-svg" height="260" viewBox="0 0 120 120" width="260">
|
||||
<!-- 5 個同心五邊形網格 (背景線) -->
|
||||
<polygon
|
||||
v-for="grid in [20, 40, 60, 80, 100]"
|
||||
:key="grid"
|
||||
fill="none"
|
||||
:points="getPolygonPoints(grid)"
|
||||
stroke="#E0E0E0"
|
||||
stroke-width="0.3"
|
||||
/>
|
||||
|
||||
<!-- 五個維度軸線 -->
|
||||
<line
|
||||
v-for="i in 5"
|
||||
:key="`axis-${i}`"
|
||||
stroke="#E0E0E0"
|
||||
stroke-width="0.3"
|
||||
x1="60"
|
||||
:x2="getAxisEndPoint(i - 1).x"
|
||||
y1="60"
|
||||
:y2="getAxisEndPoint(i - 1).y"
|
||||
/>
|
||||
|
||||
<!-- 同齡參考區間 (同齡常模基準:設為 65 分) -->
|
||||
<polygon
|
||||
fill="rgba(33, 150, 243, 0.12)"
|
||||
:points="getPolygonPoints(65)"
|
||||
stroke="#2196F3"
|
||||
stroke-dasharray="1 1"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
|
||||
<!-- 使用者真實數據填充面 -->
|
||||
<polygon
|
||||
fill="rgba(71, 194, 166, 0.35)"
|
||||
:points="userDataPoints"
|
||||
stroke="#1D7063"
|
||||
stroke-width="1.2"
|
||||
/>
|
||||
|
||||
<!-- 使用者數據頂點標記圓圈 -->
|
||||
<circle
|
||||
v-for="(pt, idx) in userDataCoordArray"
|
||||
:key="`dot-${idx}`"
|
||||
:cx="pt.x"
|
||||
:cy="pt.y"
|
||||
fill="#E66926"
|
||||
r="1.5"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
|
||||
<!-- 頂點文字標記 (Cardio, Strength, Balance, Flexibility, Agility) -->
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
x="60"
|
||||
y="8"
|
||||
>
|
||||
心肺 ({{ store.radarData.cardio }})
|
||||
</text>
|
||||
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="start"
|
||||
x="110"
|
||||
y="44"
|
||||
>
|
||||
肌力 ({{ store.radarData.strength }})
|
||||
</text>
|
||||
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="start"
|
||||
x="94"
|
||||
y="104"
|
||||
>
|
||||
平衡 ({{ store.radarData.balance }})
|
||||
</text>
|
||||
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="end"
|
||||
x="26"
|
||||
y="104"
|
||||
>
|
||||
柔軟 ({{ store.radarData.flexibility }})
|
||||
</text>
|
||||
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="end"
|
||||
x="10"
|
||||
y="44"
|
||||
>
|
||||
敏捷 ({{ store.radarData.agility }})
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
<!-- 圖例標示 -->
|
||||
<div class="radar-legend d-flex flex-column">
|
||||
<div class="d-flex align-center text-caption mb-1">
|
||||
<span class="legend-color bg-primary mr-1" />
|
||||
<span>我的指標</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center text-caption">
|
||||
<span class="legend-color bg-blue mr-1 border-dashed" />
|
||||
<span>同齡參考</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- 下一步行動引導 (根據量測進度動態顯示) -->
|
||||
<h3 class="text-subtitle-2 font-weight-black text-primary mb-3">下一步行動推薦</h3>
|
||||
|
||||
<!-- 情況 1:普測量測正在進行中 -->
|
||||
<v-card
|
||||
v-if="isIncompleteMeasurement"
|
||||
class="rounded-xl elevation-3 mb-4 transition-card text-left"
|
||||
color="surface"
|
||||
to="/checkin"
|
||||
>
|
||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 text-white elevation-1" color="secondary" size="48">
|
||||
<v-icon icon="mdi-run-fast" />
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
||||
繼續現場體能普測
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey">
|
||||
目前已完成 {{ completedCount }} / 11 站,繼續完成登錄。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 情況 2:量測全部完成,提醒可以看運動處方任務 -->
|
||||
<v-card
|
||||
v-else
|
||||
class="rounded-xl elevation-3 mb-4 transition-card text-left"
|
||||
color="surface"
|
||||
@click="prescriptionDialog = true"
|
||||
>
|
||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 text-white elevation-1" color="secondary" size="48">
|
||||
<v-icon icon="mdi-calendar-multiselect" />
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
||||
執行本週運動處方課程
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey">
|
||||
依普測結果產生,肌力 2 次、心肺 2 次、平衡 3 次。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 普測預約快捷卡片 -->
|
||||
<v-card
|
||||
class="rounded-xl elevation-3 mb-6 transition-card text-left"
|
||||
color="surface"
|
||||
to="/appointment"
|
||||
>
|
||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 text-white elevation-1" color="primary" size="48">
|
||||
<v-icon icon="mdi-calendar-clock" />
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
||||
預約 30 秒坐站與平衡回測
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey">4 週 / 12 週定期回測,動態追蹤體能進步狀態。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 最近一次量測結果摘要 -->
|
||||
<v-card class="rounded-xl elevation-3 mb-8 pa-4" color="surface">
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<h3 class="text-subtitle-1 font-weight-black text-primary d-flex align-center">
|
||||
<v-icon class="mr-2" color="secondary" icon="mdi-file-eye-outline" />
|
||||
最近量測數值摘要
|
||||
</h3>
|
||||
|
||||
<v-btn
|
||||
class="font-weight-bold px-0"
|
||||
color="primary"
|
||||
size="small"
|
||||
to="/checkin"
|
||||
variant="text"
|
||||
>查看全部</v-btn>
|
||||
</div>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="item in recentItems" :key="item.name" class="mb-2" cols="6">
|
||||
<div class="bg-grey-lighten-4 rounded-lg pa-3 h-100">
|
||||
<div class="text-caption text-grey-darken-1 mb-1">{{ item.name }}</div>
|
||||
|
||||
<div class="text-body-1 font-weight-black text-grey-darken-4">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
||||
<!-- 運動處方對話框 -->
|
||||
<v-dialog v-model="prescriptionDialog" 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-dumbbell" />
|
||||
本週運動處方明細
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pt-4 text-body-1">
|
||||
<h4 class="font-weight-bold text-primary mb-2">每週訓練任務進度</h4>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-space-between align-center text-caption mb-1">
|
||||
<span>任務完成度 (2/7堂課)</span>
|
||||
<span>28%</span>
|
||||
</div>
|
||||
|
||||
<v-progress-linear color="secondary" height="6" model-value="28" rounded />
|
||||
</div>
|
||||
|
||||
<!-- 動作說明清單 -->
|
||||
<v-list class="pa-0" density="comfortable">
|
||||
<v-list-item class="bg-teal-lighten-5 rounded-lg mb-2 py-2">
|
||||
<template #prepend>
|
||||
<v-icon color="secondary" icon="mdi-numeric-1-circle" size="24" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="font-weight-bold">下肢肌力:坐站起立訓練</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="text-caption mt-1">
|
||||
每週 2 次,每次 3 組,每組 10-12 次。<br />
|
||||
<span class="text-error font-weight-bold">* 扶手保護,站起時吐氣。</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="bg-teal-lighten-5 rounded-lg mb-2 py-2">
|
||||
<template #prepend>
|
||||
<v-icon color="secondary" icon="mdi-numeric-2-circle" size="24" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="font-weight-bold">靜態平衡:扶椅單腳站立</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="text-caption mt-1">
|
||||
每週 3 次,左右腳各 30 秒,重複 3 次。<br />
|
||||
<span class="text-grey-darken-1">手輕扶椅背,確保安全。</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="bg-teal-lighten-5 rounded-lg py-2">
|
||||
<template #prepend>
|
||||
<v-icon color="secondary" icon="mdi-numeric-3-circle" size="24" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="font-weight-bold">心肺耐力:每日快走</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="text-caption mt-1">
|
||||
每日 20-30 分鐘,達到微喘但仍可說話的強度。<br />
|
||||
<span class="text-error font-weight-bold">* 若感覺頭暈或膝蓋疼痛請暫停!</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</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="prescriptionDialog = false"
|
||||
>確認關閉</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="text-center mb-6">
|
||||
<v-avatar class="elevation-3 mb-4 border-accent-light" color="primary" size="72">
|
||||
<v-icon color="accent" icon="mdi-heart-pulse" size="42" />
|
||||
</v-avatar>
|
||||
|
||||
<h1 class="text-h5 font-weight-black text-primary mb-2">運動玩轉健康力</h1>
|
||||
|
||||
<p class="text-body-2 text-grey-darken-1 mb-0">
|
||||
不登入也可以先完成線上健康自評,登入後可預約普測與查看量測紀錄。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-card class="rounded-xl elevation-3 border-accent-light mb-4" color="surface">
|
||||
<v-card-text class="pa-5">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-avatar class="mr-3 text-white" color="secondary" size="48">
|
||||
<v-icon icon="mdi-clipboard-text-outline" />
|
||||
</v-avatar>
|
||||
|
||||
<div class="text-left">
|
||||
<div class="text-subtitle-1 font-weight-black text-primary">先做線上健康自評</div>
|
||||
<div class="text-caption text-grey">約 3 分鐘完成,取得初步健康風險分級。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold mb-3"
|
||||
color="primary"
|
||||
prepend-icon="mdi-arrow-right-circle"
|
||||
to="/self-assessment"
|
||||
variant="flat"
|
||||
>
|
||||
開始健康自評
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold"
|
||||
color="primary"
|
||||
prepend-icon="mdi-login"
|
||||
to="/login"
|
||||
variant="outlined"
|
||||
>
|
||||
登入健康平台
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="rounded-xl elevation-1 border-accent-light" color="surface">
|
||||
<v-card-text class="pa-4 d-flex align-center text-left">
|
||||
<v-icon class="mr-3" color="primary" icon="mdi-lock-outline" size="28" />
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div>
|
||||
|
||||
<div class="text-caption text-grey">
|
||||
普測預約、現場報到、量測紀錄與運動處方需要登入狀態。
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
const prescriptionDialog = ref(false)
|
||||
|
||||
const todayTimeString = computed(() => {
|
||||
const now = new Date()
|
||||
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
})
|
||||
|
||||
// 功能儲備評估
|
||||
const functionalReserve = computed(() => {
|
||||
const score = store.healthScore
|
||||
if (score >= 80) return '良好 (無衰弱風險)'
|
||||
if (score >= 65) return '正常 (需持續運動維持)'
|
||||
return '待加強 (有衰弱與跌倒風險)'
|
||||
})
|
||||
|
||||
// 每週處方文字
|
||||
const currentWeeklyPrescription = computed(() => {
|
||||
if (store.healthScore >= 80) {
|
||||
return '本週處方:1次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
||||
} else if (store.healthScore >= 65) {
|
||||
return '本週處方:2次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
||||
} else {
|
||||
return '本週處方:3次防跌平衡引導 + 每日15分鐘扶椅坐站與慢走'
|
||||
}
|
||||
})
|
||||
|
||||
// 檢測是否還有未完成的測量站
|
||||
const isIncompleteMeasurement = computed(() => {
|
||||
return store.completedStationsCount < store.totalStationsCount
|
||||
})
|
||||
|
||||
const completedCount = computed(() => store.completedStationsCount)
|
||||
|
||||
// 最近一次量測結果摘要列表
|
||||
const recentItems = computed(() => {
|
||||
const m = store.measurements
|
||||
const items = []
|
||||
|
||||
if (m.BloodPressure.completed) {
|
||||
items.push({
|
||||
name: '血壓量測',
|
||||
value: `${m.BloodPressure.values.systolic}/${m.BloodPressure.values.diastolic} mmHg`,
|
||||
})
|
||||
}
|
||||
if (m.BodyFat.completed) {
|
||||
items.push({ name: '體組成 BMI', value: `${m.BodyFat.values.bmi}` })
|
||||
}
|
||||
if (m.GripStrength.completed) {
|
||||
items.push({ name: '右手最大握力', value: `${m.GripStrength.values.rightHand} kg` })
|
||||
} else {
|
||||
items.push({ name: '左右握力', value: '未測量' })
|
||||
}
|
||||
if (m.OneLegTest.completed) {
|
||||
items.push({
|
||||
name: '單腳站立平衡',
|
||||
value: `${Math.max(m.OneLegTest.values.leftSeconds, m.OneLegTest.values.rightSeconds)} 秒`,
|
||||
})
|
||||
} else {
|
||||
items.push({ name: '開眼單足立', value: '未測量' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// --- SVG 雷達圖點計算邏輯 ---
|
||||
const cx = 60
|
||||
const cy = 60
|
||||
const r = 45
|
||||
|
||||
// 計算 5 角多邊形的頂點
|
||||
// 頂點順序: i=0(心肺), i=1(肌力), i=2(平衡), i=3(柔軟), i=4(敏捷)
|
||||
function getAxisEndPoint (i: number, radius = r) {
|
||||
const angle = -Math.PI / 2 + (i * 2 * Math.PI) / 5
|
||||
return {
|
||||
x: cx + radius * Math.cos(angle),
|
||||
y: cy + radius * Math.sin(angle),
|
||||
}
|
||||
}
|
||||
|
||||
// 根據百分比半徑繪製背景網格五邊形
|
||||
function getPolygonPoints (percent: number) {
|
||||
const radius = (percent / 100) * r
|
||||
const pts = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const pt = getAxisEndPoint(i, radius)
|
||||
pts.push(`${pt.x},${pt.y}`)
|
||||
}
|
||||
return pts.join(' ')
|
||||
}
|
||||
|
||||
// 使用者五維數據對應的雷達圖頂點座標
|
||||
const userDataCoordArray = computed(() => {
|
||||
const data = store.radarData
|
||||
// 順序與頂點對應:
|
||||
// 0: cardio (心肺), 1: strength (肌力), 2: balance (平衡), 3: flexibility (柔軟), 4: agility (敏捷)
|
||||
const values = [data.cardio, data.strength, data.balance, data.flexibility, data.agility]
|
||||
return values.map((val, i) => {
|
||||
const radius = (Math.max(5, Math.min(100, val)) / 100) * r
|
||||
return getAxisEndPoint(i, radius)
|
||||
})
|
||||
})
|
||||
|
||||
const userDataPoints = computed(() => {
|
||||
return userDataCoordArray.value.map(pt => `${pt.x},${pt.y}`).join(' ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.passport-card {
|
||||
background: linear-gradient(135deg, #1d7063 0%, #145248 100%);
|
||||
border: 1px solid rgba(71, 194, 166, 0.3);
|
||||
box-shadow: 0 8px 16px rgba(20, 82, 72, 0.25) !important;
|
||||
}
|
||||
|
||||
.bg-circle-1 {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 50%;
|
||||
background: rgba(71, 194, 166, 0.15);
|
||||
top: -50px;
|
||||
right: -50px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bg-circle-2 {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: rgba(230, 105, 38, 0.1);
|
||||
bottom: -30px;
|
||||
left: -20px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.z-index-1 {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bg-white-20 {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.bg-white-10 {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.text-white-70 {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.text-accent-light {
|
||||
color: #8cebd2 !important;
|
||||
}
|
||||
|
||||
.score-disk {
|
||||
background: radial-gradient(circle, #e66926 0%, #cc561b 100%);
|
||||
border: 3px solid #ffffff;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.score-val {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.score-unit {
|
||||
font-size: 0.65rem;
|
||||
font-weight: bold;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.border-accent-light {
|
||||
border: 1px solid rgba(71, 194, 166, 0.2) !important;
|
||||
}
|
||||
|
||||
.transition-card {
|
||||
transition: all 0.25s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.transition-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 雷達圖圖例 */
|
||||
.radar-legend {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.border-dashed {
|
||||
border: 1px dashed #2196f3;
|
||||
background-color: rgba(33, 150, 243, 0.12) !important;
|
||||
}
|
||||
|
||||
.radar-svg text {
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<v-app class="bg-gradient">
|
||||
<v-container class="fill-height px-4 d-flex justify-center align-center">
|
||||
<div class="w-100 max-width-mobile text-center">
|
||||
<!-- Logo 與品牌名稱 -->
|
||||
<div class="mb-8 animate-fade-in">
|
||||
<v-avatar class="elevation-4 mb-3 border-accent" color="primary" size="80">
|
||||
<v-icon color="accent" icon="mdi-heart-pulse" size="48" />
|
||||
</v-avatar>
|
||||
|
||||
<!-- <h1 class="text-h4 font-weight-black text-primary mb-1">運動玩轉健康力</h1>
|
||||
<p class="text-subtitle-1 text-grey-darken-1 font-weight-medium">Taipei Masters Hub</p> -->
|
||||
</div>
|
||||
|
||||
<!-- 登入卡片 -->
|
||||
<v-card class="elevation-12 rounded-xl pa-6 border-accent-light" color="surface">
|
||||
<v-card-text>
|
||||
<div class="text-left mb-6">
|
||||
<h2 class="text-h5 font-weight-bold text-primary mb-1">市民登入</h2>
|
||||
<p class="text-body-2 text-grey">請輸入您的手機號碼或信箱以進入健康護照</p>
|
||||
</div>
|
||||
|
||||
<!-- 表單 -->
|
||||
<v-form @submit.prevent="handleLogin">
|
||||
<v-text-field
|
||||
v-model="account"
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
hide-details="auto"
|
||||
label="手機號碼 / 信箱"
|
||||
placeholder="例如: 0912345678"
|
||||
prepend-inner-icon="mdi-cellphone-android"
|
||||
required
|
||||
rounded="lg"
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
class="mb-6"
|
||||
color="primary"
|
||||
hide-details="auto"
|
||||
label="密碼 (預設可留空)"
|
||||
placeholder="請輸入密碼"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
rounded="lg"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold elevation-2 mb-4 text-button"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
登入健康平台
|
||||
</v-btn>
|
||||
</v-form>
|
||||
|
||||
<div class="d-flex align-center justify-center my-4">
|
||||
<v-divider class="mr-3" />
|
||||
<span class="text-caption text-grey font-weight-bold">或使用以下方式快捷登入</span>
|
||||
<v-divider class="ml-3" />
|
||||
</div>
|
||||
|
||||
<!-- LINE 一鍵登入 (高齡場景極度實用) -->
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold text-white mb-4 text-button d-flex align-center justify-center"
|
||||
color="#06C755"
|
||||
:loading="lineLoading"
|
||||
size="large"
|
||||
style="text-transform: none"
|
||||
variant="flat"
|
||||
@click="handleLineLogin"
|
||||
>
|
||||
<v-icon class="mr-2" icon="mdi-message-text" />
|
||||
使用 LINE 帳號一鍵登入
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="justify-center">
|
||||
<span class="text-caption text-grey">
|
||||
登入即表示您同意本平台的
|
||||
<a class="text-accent text-decoration-none font-weight-bold" href="#">個資授權條款</a>
|
||||
</span>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<!-- 底部輔助說明 -->
|
||||
<div class="mt-8 text-caption text-grey-darken-1">
|
||||
<p>© 2026 SKYTEK</p>
|
||||
<p>技術支援: 健康中心</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const account = ref('0912-345-678')
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
const lineLoading = ref(false)
|
||||
|
||||
function handleLogin() {
|
||||
if (!account.value) return
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
store.login(account.value)
|
||||
loading.value = false
|
||||
router.push('/')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function handleLineLogin() {
|
||||
lineLoading.value = true
|
||||
setTimeout(() => {
|
||||
store.login('0912-345-678') // LINE 模擬登入預設手機
|
||||
lineLoading.value = false
|
||||
router.push('/')
|
||||
}, 1200)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bg-gradient {
|
||||
background: linear-gradient(135deg, #f0f9f6 0%, #dcefea 100%) !important;
|
||||
}
|
||||
|
||||
.border-accent {
|
||||
border: 3px solid #47c2a6 !important;
|
||||
}
|
||||
|
||||
.border-accent-light {
|
||||
border: 1px solid rgba(71, 194, 166, 0.2) !important;
|
||||
}
|
||||
|
||||
.max-width-mobile {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
/* 微小動畫 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,465 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<template #main>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="text-h5 font-weight-black text-primary">線上健康自評</h1>
|
||||
<p class="text-caption text-grey">只需 3 分鐘,評估您的運動風險與健康狀態</p>
|
||||
</div>
|
||||
|
||||
<!-- 進度指示器 -->
|
||||
<v-row class="mb-6 justify-center" dense>
|
||||
<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-caption">{{ idx + 1 }}</span>
|
||||
</v-avatar>
|
||||
|
||||
<div
|
||||
class="text-caption 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 border-accent-light mb-6" color="surface">
|
||||
<v-card-text class="pa-5">
|
||||
<!-- 步驟 1:基本資料 -->
|
||||
<div v-if="currentStep === 0">
|
||||
<h2 class="text-subtitle-1 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 dense>
|
||||
<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-text-field
|
||||
v-model="profileForm.birthday"
|
||||
class="mb-3"
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
label="生日"
|
||||
type="date"
|
||||
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-subtitle-1 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-2 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-2 font-weight-bold text-grey-darken-2 mb-2">2. 您目前最想改善的身體功能? (可複選)</p>
|
||||
|
||||
<v-row dense>
|
||||
<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-caption font-weight-bold">{{ imp }}</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- 步驟 3:身體狀況自評 -->
|
||||
<div v-if="currentStep === 2">
|
||||
<h2 class="text-subtitle-1 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-2 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-2 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-2 font-weight-bold text-grey-darken-2 mb-2">3. 您有哪些慢性疾病? (可複選)</p>
|
||||
|
||||
<v-row class="mb-4" dense>
|
||||
<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>
|
||||
|
||||
<v-text-field
|
||||
v-model="medicationInput"
|
||||
append-inner-icon="mdi-plus"
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
label="用藥提醒設定 (例如:每日晨間降血壓藥)"
|
||||
placeholder="請輸入常用藥物或提醒"
|
||||
variant="outlined"
|
||||
@click:append-inner="addMedication"
|
||||
@keyup.enter="addMedication"
|
||||
/>
|
||||
|
||||
<div v-if="assessmentForm.medicationReminders.length > 0" class="d-flex flex-wrap gap-2 mt-2">
|
||||
<v-chip
|
||||
v-for="(med, idx) in assessmentForm.medicationReminders"
|
||||
:key="idx"
|
||||
closable
|
||||
color="secondary"
|
||||
size="small"
|
||||
@click:close="removeMedication(idx)"
|
||||
>
|
||||
{{ med }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</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-h5 font-weight-bold mb-1" :class="`text-${getResultColor(resultLevel)}`">
|
||||
自評分級:{{ resultLevel }}
|
||||
</h2>
|
||||
|
||||
<p class="text-subtitle-2 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-2 text-grey-darken-3 mb-2">
|
||||
您的身體功能與日常活動習慣良好。建議您每週繼續保持 3 次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動處方。
|
||||
</p>
|
||||
|
||||
<p v-else-if="resultLevel === '需追蹤'" class="text-body-2 text-grey-darken-3 mb-2">
|
||||
由於您運動頻率偏低,或是過去有跌倒史,建議您可以前往運動中心預約專業普測,由現場教練為您提供適合的肌力與平衡回測建議。
|
||||
</p>
|
||||
|
||||
<p v-else class="text-body-2 text-grey-darken-3 mb-2">
|
||||
檢測到您有多項慢性病或中重度疼痛,運動時請特別留意。建議您先向您的家庭醫師或社區據點諮詢,並在運動普測現場合對您的健康狀態,以確保運動安全。
|
||||
</p>
|
||||
|
||||
<div class="font-weight-bold text-caption 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="currentStep--"
|
||||
>
|
||||
上一步
|
||||
</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="currentStep++"
|
||||
>
|
||||
下一步
|
||||
</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 { 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],
|
||||
medicationReminders: [...store.selfAssessment.medicationReminders],
|
||||
})
|
||||
|
||||
const medicationInput = ref('')
|
||||
const resultLevel = ref(store.selfAssessment.scoreLevel)
|
||||
|
||||
const improvementsOptions = ['肌力', '平衡', '心肺', '柔軟度', '敏捷', '體脂/BMI']
|
||||
const diseaseOptions = ['高血壓', '糖尿病', '高血脂', '心臟病', '關節退化', '骨質疏鬆']
|
||||
|
||||
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-stretching'
|
||||
}
|
||||
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 addMedication () {
|
||||
if (medicationInput.value.trim()) {
|
||||
assessmentForm.medicationReminders.push(medicationInput.value.trim())
|
||||
medicationInput.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function removeMedication (index: number) {
|
||||
assessmentForm.medicationReminders.splice(index, 1)
|
||||
}
|
||||
|
||||
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,
|
||||
medicationReminders: assessmentForm.medicationReminders,
|
||||
})
|
||||
|
||||
// 取得計算後的結果
|
||||
resultLevel.value = store.selfAssessment.scoreLevel
|
||||
currentStep.value = 3
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user