399 lines
10 KiB
TypeScript
399 lines
10 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { computed, ref } from 'vue'
|
|
|
|
export interface UserProfile {
|
|
name: string
|
|
birthday: string
|
|
gender: string
|
|
height: number
|
|
weight: number
|
|
phone: string
|
|
email: string
|
|
emergencyContact: string
|
|
emergencyPhone: string
|
|
consent: boolean
|
|
}
|
|
|
|
export interface SelfAssessment {
|
|
exerciseFrequency: string
|
|
painLevel: string
|
|
hasFalls: boolean
|
|
chronicDiseases: string[]
|
|
wantedImprovements: string[]
|
|
scoreLevel: string
|
|
completed: boolean
|
|
}
|
|
|
|
export interface Appointment {
|
|
id: string
|
|
title: string
|
|
date: string
|
|
time: string
|
|
location: string
|
|
status: '未報名' | '已報名' | '已報到'
|
|
slots: number
|
|
organizer: string
|
|
items: string[]
|
|
}
|
|
|
|
export interface MeasurementStation {
|
|
name: string
|
|
completed: boolean
|
|
time?: string
|
|
values: Record<string, any>
|
|
}
|
|
|
|
function createDefaultUserProfile (): UserProfile {
|
|
return {
|
|
name: '林大明',
|
|
birthday: '1961-08-15',
|
|
gender: '男',
|
|
height: 168,
|
|
weight: 65,
|
|
phone: '0912-345-678',
|
|
email: 'jianguo.lin@email.com',
|
|
emergencyContact: '林小明 (長子)',
|
|
emergencyPhone: '0987-654-321',
|
|
consent: true,
|
|
}
|
|
}
|
|
|
|
function createDefaultSelfAssessment (): SelfAssessment {
|
|
return {
|
|
exerciseFrequency: '1-2',
|
|
painLevel: '輕微',
|
|
hasFalls: false,
|
|
chronicDiseases: ['高血壓'],
|
|
wantedImprovements: ['肌力', '平衡'],
|
|
scoreLevel: '需追蹤',
|
|
completed: true,
|
|
}
|
|
}
|
|
|
|
function createDefaultAppointments (): Appointment[] {
|
|
const measurementItems = [
|
|
'血壓',
|
|
'體脂',
|
|
'握力',
|
|
'5次坐站',
|
|
'30秒椅子坐立',
|
|
'原地抬膝踏步',
|
|
'手臂屈舉',
|
|
'抓背測驗',
|
|
'坐姿體前彎',
|
|
'2.44米繞物',
|
|
'開眼單足立',
|
|
]
|
|
const location = '信義區運動中心 3 樓多功能教室'
|
|
const organizer = '臺北市政府體育局'
|
|
|
|
return [
|
|
{
|
|
id: 'apt-01',
|
|
title: '銀髮活力公益普測 A 時段',
|
|
date: '2026-07-04',
|
|
time: '09:00 - 10:00',
|
|
location,
|
|
status: '已報名',
|
|
slots: 12,
|
|
organizer,
|
|
items: measurementItems,
|
|
},
|
|
{
|
|
id: 'apt-02',
|
|
title: '銀髮活力公益普測 B 時段',
|
|
date: '2026-07-04',
|
|
time: '10:30 - 11:30',
|
|
location,
|
|
status: '已報到',
|
|
slots: 5,
|
|
organizer,
|
|
items: measurementItems,
|
|
},
|
|
{
|
|
id: 'apt-03',
|
|
title: '銀髮活力公益普測 C 時段',
|
|
date: '2026-07-04',
|
|
time: '13:30 - 14:30',
|
|
location,
|
|
status: '未報名',
|
|
slots: 45,
|
|
organizer,
|
|
items: measurementItems,
|
|
},
|
|
]
|
|
}
|
|
|
|
function createDefaultMeasurements (): Record<string, MeasurementStation> {
|
|
return {
|
|
BloodPressure: {
|
|
name: '血壓量測',
|
|
completed: true,
|
|
time: '2026-06-18 15:00',
|
|
values: { systolic: 132, diastolic: 84, heartRate: 72 },
|
|
},
|
|
BodyFat: {
|
|
name: '體組成分析',
|
|
completed: true,
|
|
time: '2026-06-18 15:05',
|
|
values: { bmi: 23, bodyFatPct: 24.5 },
|
|
},
|
|
GripStrength: {
|
|
name: '左右握力',
|
|
completed: false,
|
|
values: { leftHand: 0, rightHand: 0 },
|
|
},
|
|
Sit5x: {
|
|
name: '5次坐站測試',
|
|
completed: false,
|
|
values: { durationSeconds: 0 },
|
|
},
|
|
Sit30s: {
|
|
name: '30秒椅子坐立',
|
|
completed: false,
|
|
values: { count: 0 },
|
|
},
|
|
StepTest: {
|
|
name: '2分鐘原地抬膝踏步',
|
|
completed: false,
|
|
values: { count: 0 },
|
|
},
|
|
ArmTest: {
|
|
name: '30秒肱二頭肌手臂屈舉',
|
|
completed: false,
|
|
values: { count: 0 },
|
|
},
|
|
BackTest: {
|
|
name: '抓背測驗(柔軟度)',
|
|
completed: false,
|
|
values: { leftDistanceCm: 0, rightDistanceCm: 0 },
|
|
},
|
|
SitReachTest: {
|
|
name: '坐姿體前彎(柔軟度)',
|
|
completed: false,
|
|
values: { distanceCm: 0 },
|
|
},
|
|
UpGo8ft: {
|
|
name: '2.44公尺起立繞物(敏捷)',
|
|
completed: false,
|
|
values: { durationSeconds: 0 },
|
|
},
|
|
OneLegTest: {
|
|
name: '開眼單足立(平衡)',
|
|
completed: false,
|
|
values: { leftSeconds: 0, rightSeconds: 0 },
|
|
},
|
|
}
|
|
}
|
|
|
|
function createDefaultRadarData () {
|
|
return {
|
|
strength: 65,
|
|
balance: 60,
|
|
flexibility: 55,
|
|
agility: 70,
|
|
cardio: 62,
|
|
}
|
|
}
|
|
|
|
export const useAppStore = defineStore('app', () => {
|
|
const isLoggedIn = ref(false)
|
|
const userProfile = ref<UserProfile>(createDefaultUserProfile())
|
|
const selfAssessment = ref<SelfAssessment>(createDefaultSelfAssessment())
|
|
const appointments = ref<Appointment[]>(createDefaultAppointments())
|
|
const measurements = ref<Record<string, MeasurementStation>>(createDefaultMeasurements())
|
|
const healthScore = ref(72)
|
|
const radarData = ref(createDefaultRadarData())
|
|
const isReportGenerated = ref(false)
|
|
|
|
const completedStationsCount = computed(() => {
|
|
return Object.values(measurements.value).filter(station => station.completed).length
|
|
})
|
|
|
|
const totalStationsCount = computed(() => {
|
|
return Object.keys(measurements.value).length
|
|
})
|
|
|
|
const progressPercent = computed((): number => {
|
|
const keys = Object.keys(measurements.value)
|
|
if (keys.length === 0) {
|
|
return 0
|
|
}
|
|
|
|
const done = Object.values(measurements.value).filter(station => station.completed).length
|
|
return Math.round((done / keys.length) * 100)
|
|
})
|
|
|
|
function login (phone: string) {
|
|
isLoggedIn.value = true
|
|
userProfile.value.phone = phone
|
|
}
|
|
|
|
function logout () {
|
|
isLoggedIn.value = false
|
|
}
|
|
|
|
function updateProfile (profile: Partial<UserProfile>) {
|
|
userProfile.value = { ...userProfile.value, ...profile }
|
|
}
|
|
|
|
function submitSelfAssessment (answers: Partial<SelfAssessment>) {
|
|
selfAssessment.value = {
|
|
...selfAssessment.value,
|
|
...answers,
|
|
completed: true,
|
|
}
|
|
|
|
let level = '低風險'
|
|
if (answers.exerciseFrequency === '0' || answers.hasFalls === true) {
|
|
level = '需追蹤'
|
|
}
|
|
|
|
if (
|
|
answers.painLevel === '中度'
|
|
|| answers.painLevel === '重度'
|
|
|| (answers.chronicDiseases && answers.chronicDiseases.length >= 2)
|
|
) {
|
|
level = '建議諮詢'
|
|
}
|
|
|
|
selfAssessment.value.scoreLevel = level
|
|
}
|
|
|
|
function registerAppointment (appointmentId: string) {
|
|
const appointment = appointments.value.find(item => item.id === appointmentId)
|
|
if (!appointment) {
|
|
return
|
|
}
|
|
|
|
appointment.status = '已報名'
|
|
appointment.slots -= 1
|
|
}
|
|
|
|
function cancelAppointment (appointmentId: string) {
|
|
const appointment = appointments.value.find(item => item.id === appointmentId)
|
|
if (!appointment) {
|
|
return
|
|
}
|
|
|
|
appointment.status = '未報名'
|
|
appointment.slots += 1
|
|
}
|
|
|
|
function checkinAppointment (appointmentId: string) {
|
|
const appointment = appointments.value.find(item => item.id === appointmentId)
|
|
if (!appointment) {
|
|
return
|
|
}
|
|
|
|
appointment.status = '已報到'
|
|
}
|
|
|
|
function saveMeasurement (code: string, values: Record<string, any>) {
|
|
const measurement = measurements.value[code]
|
|
if (!measurement) {
|
|
return
|
|
}
|
|
|
|
const now = new Date()
|
|
const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
|
|
|
measurement.values = { ...values }
|
|
measurement.completed = true
|
|
measurement.time = timeStr
|
|
}
|
|
|
|
function generateFinalReport () {
|
|
const currentMeasurements = measurements.value
|
|
|
|
let strScore = 65
|
|
if (currentMeasurements.GripStrength.completed) {
|
|
const avgGrip
|
|
= ((currentMeasurements.GripStrength.values.leftHand || 0)
|
|
+ (currentMeasurements.GripStrength.values.rightHand || 0))
|
|
/ 2
|
|
const limit = userProfile.value.gender === '男' ? 30 : 20
|
|
strScore = Math.min(95, Math.max(50, Math.round((avgGrip / limit) * 75)))
|
|
}
|
|
|
|
let balScore = 60
|
|
if (currentMeasurements.OneLegTest.completed) {
|
|
const maxSec = Math.max(
|
|
currentMeasurements.OneLegTest.values.leftSeconds || 0,
|
|
currentMeasurements.OneLegTest.values.rightSeconds || 0,
|
|
)
|
|
balScore = Math.min(98, Math.max(40, Math.round((maxSec / 30) * 90)))
|
|
}
|
|
|
|
let flexScore = 55
|
|
if (currentMeasurements.SitReachTest.completed) {
|
|
const dist = currentMeasurements.SitReachTest.values.distanceCm || 0
|
|
flexScore = Math.min(95, Math.max(45, 60 + dist * 1.5))
|
|
}
|
|
|
|
let agiScore = 70
|
|
if (currentMeasurements.UpGo8ft.completed) {
|
|
const sec = currentMeasurements.UpGo8ft.values.durationSeconds || 10
|
|
agiScore = Math.min(95, Math.max(40, Math.round(100 - sec * 5)))
|
|
}
|
|
|
|
let cardScore = 62
|
|
if (currentMeasurements.StepTest.completed) {
|
|
const count = currentMeasurements.StepTest.values.count || 60
|
|
cardScore = Math.min(95, Math.max(45, Math.round((count / 80) * 80)))
|
|
}
|
|
|
|
radarData.value = {
|
|
strength: strScore,
|
|
balance: balScore,
|
|
flexibility: flexScore,
|
|
agility: agiScore,
|
|
cardio: cardScore,
|
|
}
|
|
|
|
const avg = Math.round((strScore + balScore + flexScore + agiScore + cardScore) / 5)
|
|
healthScore.value = Math.min(100, avg + 5)
|
|
isReportGenerated.value = true
|
|
}
|
|
|
|
function resetMeasurements () {
|
|
for (const [key, measurement] of Object.entries(measurements.value)) {
|
|
if (key !== 'BloodPressure' && key !== 'BodyFat') {
|
|
measurement.completed = false
|
|
|
|
for (const valueKey of Object.keys(measurement.values)) {
|
|
measurement.values[valueKey] = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
isReportGenerated.value = false
|
|
healthScore.value = 72
|
|
radarData.value = createDefaultRadarData()
|
|
}
|
|
|
|
return {
|
|
isLoggedIn,
|
|
userProfile,
|
|
selfAssessment,
|
|
appointments,
|
|
measurements,
|
|
healthScore,
|
|
radarData,
|
|
isReportGenerated,
|
|
completedStationsCount,
|
|
totalStationsCount,
|
|
progressPercent,
|
|
login,
|
|
logout,
|
|
updateProfile,
|
|
submitSelfAssessment,
|
|
registerAppointment,
|
|
cancelAppointment,
|
|
checkinAppointment,
|
|
saveMeasurement,
|
|
generateFinalReport,
|
|
resetMeasurements,
|
|
}
|
|
})
|