chore: OXC Formatter
This commit is contained in:
@@ -1,365 +1,377 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { login as apiLogin } from '@/api/auth'
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { login as apiLogin } from "@/api/auth";
|
||||
|
||||
/** 後端尚未完成前保持 true,改為 false 才會實際呼叫登入 API */
|
||||
const USE_MOCK = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||
const USE_MOCK = import.meta.env.VITE_USE_MOCK !== "false";
|
||||
|
||||
export interface UserProfile {
|
||||
name: string
|
||||
birthday: string
|
||||
gender: string
|
||||
height: number
|
||||
weight: number
|
||||
phone: string
|
||||
email: string
|
||||
emergencyContact: string
|
||||
emergencyPhone: string
|
||||
consent: boolean
|
||||
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
|
||||
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[]
|
||||
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>
|
||||
name: string;
|
||||
completed: boolean;
|
||||
time?: string;
|
||||
values: Record<string, any>;
|
||||
}
|
||||
|
||||
function createDefaultUserProfile (): UserProfile {
|
||||
function createDefaultUserProfile(): UserProfile {
|
||||
return {
|
||||
name: '林大明',
|
||||
birthday: '1961-08-15',
|
||||
gender: '男',
|
||||
name: "林大明",
|
||||
birthday: "1961-08-15",
|
||||
gender: "男",
|
||||
height: 168,
|
||||
weight: 65,
|
||||
phone: '0912-345-678',
|
||||
email: 'jianguo.lin@email.com',
|
||||
emergencyContact: '林小明 (長子)',
|
||||
emergencyPhone: '0987-654-321',
|
||||
phone: "0912-345-678",
|
||||
email: "jianguo.lin@email.com",
|
||||
emergencyContact: "林小明 (長子)",
|
||||
emergencyPhone: "0987-654-321",
|
||||
consent: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createDefaultSelfAssessment (): SelfAssessment {
|
||||
function createDefaultSelfAssessment(): SelfAssessment {
|
||||
return {
|
||||
exerciseFrequency: '1-2',
|
||||
painLevel: '輕微',
|
||||
exerciseFrequency: "1-2",
|
||||
painLevel: "輕微",
|
||||
hasFalls: false,
|
||||
chronicDiseases: ['高血壓'],
|
||||
wantedImprovements: ['肌力', '平衡'],
|
||||
scoreLevel: '需追蹤',
|
||||
chronicDiseases: ["高血壓"],
|
||||
wantedImprovements: ["肌力", "平衡"],
|
||||
scoreLevel: "需追蹤",
|
||||
completed: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createDefaultAppointments (): Appointment[] {
|
||||
function createDefaultAppointments(): Appointment[] {
|
||||
const measurementItems = [
|
||||
'血壓',
|
||||
'體脂',
|
||||
'握力',
|
||||
'5次坐站',
|
||||
'30秒椅子坐立',
|
||||
'原地抬膝踏步',
|
||||
'手臂屈舉',
|
||||
'抓背測驗',
|
||||
'坐姿體前彎',
|
||||
'2.44米繞物',
|
||||
'開眼單足立',
|
||||
]
|
||||
const location = '信義區運動中心 3 樓多功能教室'
|
||||
const organizer = '臺北市政府體育局'
|
||||
"血壓",
|
||||
"體脂",
|
||||
"握力",
|
||||
"5次坐站",
|
||||
"30秒椅子坐立",
|
||||
"原地抬膝踏步",
|
||||
"手臂屈舉",
|
||||
"抓背測驗",
|
||||
"坐姿體前彎",
|
||||
"2.44米繞物",
|
||||
"開眼單足立",
|
||||
];
|
||||
const location = "信義區運動中心 3 樓多功能教室";
|
||||
const organizer = "臺北市政府體育局";
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'apt-01',
|
||||
title: '銀髮活力公益普測 A 時段',
|
||||
date: '2026-07-04',
|
||||
time: '09:00 - 10:00',
|
||||
id: "apt-01",
|
||||
title: "銀髮活力公益普測 A 時段",
|
||||
date: "2026-07-04",
|
||||
time: "09:00 - 10:00",
|
||||
location,
|
||||
status: '已報名',
|
||||
status: "已報名",
|
||||
slots: 12,
|
||||
organizer,
|
||||
items: measurementItems,
|
||||
},
|
||||
{
|
||||
id: 'apt-02',
|
||||
title: '銀髮活力公益普測 B 時段',
|
||||
date: '2026-07-04',
|
||||
time: '10:30 - 11:30',
|
||||
id: "apt-02",
|
||||
title: "銀髮活力公益普測 B 時段",
|
||||
date: "2026-07-04",
|
||||
time: "10:30 - 11:30",
|
||||
location,
|
||||
status: '已報到',
|
||||
status: "已報到",
|
||||
slots: 5,
|
||||
organizer,
|
||||
items: measurementItems,
|
||||
},
|
||||
{
|
||||
id: 'apt-03',
|
||||
title: '銀髮活力公益普測 C 時段',
|
||||
date: '2026-07-04',
|
||||
time: '13:30 - 14:30',
|
||||
id: "apt-03",
|
||||
title: "銀髮活力公益普測 C 時段",
|
||||
date: "2026-07-04",
|
||||
time: "13:30 - 14:30",
|
||||
location,
|
||||
status: '未報名',
|
||||
status: "未報名",
|
||||
slots: 45,
|
||||
organizer,
|
||||
items: measurementItems,
|
||||
},
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
function createDefaultMeasurements (): Record<string, MeasurementStation> {
|
||||
function createDefaultMeasurements(): Record<string, MeasurementStation> {
|
||||
return {
|
||||
BloodPressure: {
|
||||
name: '血壓量測',
|
||||
name: "血壓量測",
|
||||
completed: true,
|
||||
time: '2026-06-18 15:00',
|
||||
time: "2026-06-18 15:00",
|
||||
values: { systolic: 132, diastolic: 84, heartRate: 72 },
|
||||
},
|
||||
BodyFat: {
|
||||
name: '體組成分析',
|
||||
name: "體組成分析",
|
||||
completed: true,
|
||||
time: '2026-06-18 15:05',
|
||||
time: "2026-06-18 15:05",
|
||||
values: { bmi: 23, bodyFatPct: 24.5 },
|
||||
},
|
||||
GripStrength: {
|
||||
name: '左右握力',
|
||||
name: "左右握力",
|
||||
completed: false,
|
||||
values: { leftHand: 0, rightHand: 0 },
|
||||
},
|
||||
Sit5x: {
|
||||
name: '5次坐站測試',
|
||||
name: "5次坐站測試",
|
||||
completed: false,
|
||||
values: { durationSeconds: 0 },
|
||||
},
|
||||
Sit30s: {
|
||||
name: '30秒椅子坐立',
|
||||
name: "30秒椅子坐立",
|
||||
completed: false,
|
||||
values: { count: 0 },
|
||||
},
|
||||
StepTest: {
|
||||
name: '2分鐘原地抬膝踏步',
|
||||
name: "2分鐘原地抬膝踏步",
|
||||
completed: false,
|
||||
values: { count: 0 },
|
||||
},
|
||||
ArmTest: {
|
||||
name: '30秒肱二頭肌手臂屈舉',
|
||||
name: "30秒肱二頭肌手臂屈舉",
|
||||
completed: false,
|
||||
values: { count: 0 },
|
||||
},
|
||||
BackTest: {
|
||||
name: '抓背測驗(柔軟度)',
|
||||
name: "抓背測驗(柔軟度)",
|
||||
completed: false,
|
||||
values: { leftDistanceCm: 0, rightDistanceCm: 0 },
|
||||
},
|
||||
SitReachTest: {
|
||||
name: '坐姿體前彎(柔軟度)',
|
||||
name: "坐姿體前彎(柔軟度)",
|
||||
completed: false,
|
||||
values: { distanceCm: 0 },
|
||||
},
|
||||
UpGo8ft: {
|
||||
name: '2.44公尺起立繞物(敏捷)',
|
||||
name: "2.44公尺起立繞物(敏捷)",
|
||||
completed: false,
|
||||
values: { durationSeconds: 0 },
|
||||
},
|
||||
OneLegTest: {
|
||||
name: '開眼單足立(平衡)',
|
||||
name: "開眼單足立(平衡)",
|
||||
completed: false,
|
||||
values: { leftSeconds: 0, rightSeconds: 0 },
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createDefaultRadarData () {
|
||||
function createDefaultRadarData() {
|
||||
return {
|
||||
strength: 65,
|
||||
balance: 60,
|
||||
flexibility: 55,
|
||||
agility: 70,
|
||||
cardio: 62,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const isLoggedIn = ref(false)
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
const isLoggedIn = ref(false);
|
||||
/** 登入取得的使用者識別碼,課程建議等 API 需要此值查詢 */
|
||||
const userID = ref('')
|
||||
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 userID = ref("");
|
||||
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
|
||||
})
|
||||
return Object.values(measurements.value).filter(
|
||||
(station) => station.completed,
|
||||
).length;
|
||||
});
|
||||
|
||||
const totalStationsCount = computed(() => {
|
||||
return Object.keys(measurements.value).length
|
||||
})
|
||||
return Object.keys(measurements.value).length;
|
||||
});
|
||||
|
||||
const progressPercent = computed((): number => {
|
||||
const keys = Object.keys(measurements.value)
|
||||
const keys = Object.keys(measurements.value);
|
||||
if (keys.length === 0) {
|
||||
return 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
const done = Object.values(measurements.value).filter(station => station.completed).length
|
||||
return Math.round((done / keys.length) * 100)
|
||||
})
|
||||
const done = Object.values(measurements.value).filter(
|
||||
(station) => station.completed,
|
||||
).length;
|
||||
return Math.round((done / keys.length) * 100);
|
||||
});
|
||||
|
||||
/** 手機門號登入;後端未完成時走模擬資料,完成後設 VITE_USE_MOCK=false 改打真實 API */
|
||||
async function login (phone: string) {
|
||||
async function login(phone: string) {
|
||||
if (USE_MOCK) {
|
||||
isLoggedIn.value = true
|
||||
userID.value = 'MOCK-USER'
|
||||
userProfile.value.phone = phone
|
||||
return
|
||||
isLoggedIn.value = true;
|
||||
userID.value = "MOCK-USER";
|
||||
userProfile.value.phone = phone;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await apiLogin(phone)
|
||||
const response = await apiLogin(phone);
|
||||
|
||||
isLoggedIn.value = true
|
||||
userID.value = response.userID
|
||||
userProfile.value.phone = response.phone
|
||||
userProfile.value.name = response.userName
|
||||
isLoggedIn.value = true;
|
||||
userID.value = response.userID;
|
||||
userProfile.value.phone = response.phone;
|
||||
userProfile.value.name = response.userName;
|
||||
}
|
||||
|
||||
function logout () {
|
||||
isLoggedIn.value = false
|
||||
userID.value = ''
|
||||
function logout() {
|
||||
isLoggedIn.value = false;
|
||||
userID.value = "";
|
||||
}
|
||||
|
||||
function updateProfile (profile: Partial<UserProfile>) {
|
||||
userProfile.value = { ...userProfile.value, ...profile }
|
||||
function updateProfile(profile: Partial<UserProfile>) {
|
||||
userProfile.value = { ...userProfile.value, ...profile };
|
||||
}
|
||||
|
||||
function submitSelfAssessment (answers: Partial<SelfAssessment>) {
|
||||
function submitSelfAssessment(answers: Partial<SelfAssessment>) {
|
||||
selfAssessment.value = {
|
||||
...selfAssessment.value,
|
||||
...answers,
|
||||
completed: true,
|
||||
}
|
||||
};
|
||||
|
||||
let level = '低風險'
|
||||
if (answers.exerciseFrequency === '0' || answers.hasFalls === true) {
|
||||
level = '需追蹤'
|
||||
let level = "低風險";
|
||||
if (answers.exerciseFrequency === "0" || answers.hasFalls === true) {
|
||||
level = "需追蹤";
|
||||
}
|
||||
|
||||
if (
|
||||
answers.painLevel === '中度'
|
||||
|| answers.painLevel === '重度'
|
||||
|| (answers.chronicDiseases && answers.chronicDiseases.length >= 2)
|
||||
answers.painLevel === "中度" ||
|
||||
answers.painLevel === "重度" ||
|
||||
(answers.chronicDiseases && answers.chronicDiseases.length >= 2)
|
||||
) {
|
||||
level = '建議諮詢'
|
||||
level = "建議諮詢";
|
||||
}
|
||||
|
||||
selfAssessment.value.scoreLevel = level
|
||||
selfAssessment.value.scoreLevel = level;
|
||||
}
|
||||
|
||||
function registerAppointment (appointmentId: string) {
|
||||
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||
function registerAppointment(appointmentId: string) {
|
||||
const appointment = appointments.value.find(
|
||||
(item) => item.id === appointmentId,
|
||||
);
|
||||
if (!appointment) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
appointment.status = '已報名'
|
||||
appointment.slots -= 1
|
||||
appointment.status = "已報名";
|
||||
appointment.slots -= 1;
|
||||
}
|
||||
|
||||
function cancelAppointment (appointmentId: string) {
|
||||
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||
function cancelAppointment(appointmentId: string) {
|
||||
const appointment = appointments.value.find(
|
||||
(item) => item.id === appointmentId,
|
||||
);
|
||||
if (!appointment) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
appointment.status = '未報名'
|
||||
appointment.slots += 1
|
||||
appointment.status = "未報名";
|
||||
appointment.slots += 1;
|
||||
}
|
||||
|
||||
function checkinAppointment (appointmentId: string) {
|
||||
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||
function checkinAppointment(appointmentId: string) {
|
||||
const appointment = appointments.value.find(
|
||||
(item) => item.id === appointmentId,
|
||||
);
|
||||
if (!appointment) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
appointment.status = '已報到'
|
||||
appointment.status = "已報到";
|
||||
}
|
||||
|
||||
function saveMeasurement (code: string, values: Record<string, any>) {
|
||||
const measurement = measurements.value[code]
|
||||
function saveMeasurement(code: string, values: Record<string, any>) {
|
||||
const measurement = measurements.value[code];
|
||||
if (!measurement) {
|
||||
return
|
||||
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')}`
|
||||
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
|
||||
measurement.values = { ...values };
|
||||
measurement.completed = true;
|
||||
measurement.time = timeStr;
|
||||
}
|
||||
|
||||
function generateFinalReport () {
|
||||
const currentMeasurements = measurements.value
|
||||
function generateFinalReport() {
|
||||
const currentMeasurements = measurements.value;
|
||||
|
||||
let strScore = 65
|
||||
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)))
|
||||
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
|
||||
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)))
|
||||
);
|
||||
balScore = Math.min(98, Math.max(40, Math.round((maxSec / 30) * 90)));
|
||||
}
|
||||
|
||||
let flexScore = 55
|
||||
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))
|
||||
const dist = currentMeasurements.SitReachTest.values.distanceCm || 0;
|
||||
flexScore = Math.min(95, Math.max(45, 60 + dist * 1.5));
|
||||
}
|
||||
|
||||
let agiScore = 70
|
||||
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)))
|
||||
const sec = currentMeasurements.UpGo8ft.values.durationSeconds || 10;
|
||||
agiScore = Math.min(95, Math.max(40, Math.round(100 - sec * 5)));
|
||||
}
|
||||
|
||||
let cardScore = 62
|
||||
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)))
|
||||
const count = currentMeasurements.StepTest.values.count || 60;
|
||||
cardScore = Math.min(95, Math.max(45, Math.round((count / 80) * 80)));
|
||||
}
|
||||
|
||||
radarData.value = {
|
||||
@@ -368,27 +380,29 @@ export const useAppStore = defineStore('app', () => {
|
||||
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
|
||||
const avg = Math.round(
|
||||
(strScore + balScore + flexScore + agiScore + cardScore) / 5,
|
||||
);
|
||||
healthScore.value = Math.min(100, avg + 5);
|
||||
isReportGenerated.value = true;
|
||||
}
|
||||
|
||||
function resetMeasurements () {
|
||||
function resetMeasurements() {
|
||||
for (const [key, measurement] of Object.entries(measurements.value)) {
|
||||
if (key !== 'BloodPressure' && key !== 'BodyFat') {
|
||||
measurement.completed = false
|
||||
if (key !== "BloodPressure" && key !== "BodyFat") {
|
||||
measurement.completed = false;
|
||||
|
||||
for (const valueKey of Object.keys(measurement.values)) {
|
||||
measurement.values[valueKey] = 0
|
||||
measurement.values[valueKey] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isReportGenerated.value = false
|
||||
healthScore.value = 72
|
||||
radarData.value = createDefaultRadarData()
|
||||
isReportGenerated.value = false;
|
||||
healthScore.value = 72;
|
||||
radarData.value = createDefaultRadarData();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -414,5 +428,5 @@ export const useAppStore = defineStore('app', () => {
|
||||
saveMeasurement,
|
||||
generateFinalReport,
|
||||
resetMeasurements,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,225 +5,331 @@ import type {
|
||||
FitnessGroup,
|
||||
TimeSlot,
|
||||
YesNo,
|
||||
} from '@/api/types'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
} from "@/api/types";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
fetchCourseList,
|
||||
fetchGroup,
|
||||
fetchMorning,
|
||||
fetchNight,
|
||||
fetchNoon,
|
||||
} from '@/api/course'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
} from "@/api/course";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
/** 後端尚未完成,預設走前端模擬資料;設定 VITE_USE_MOCK=false 即改打真實 API */
|
||||
const USE_MOCK = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||
const USE_MOCK = import.meta.env.VITE_USE_MOCK !== "false";
|
||||
|
||||
/** 模擬網路延遲,讓載入狀態在開發時可被觀察到 */
|
||||
const MOCK_DELAY = 300
|
||||
const MOCK_DELAY = 300;
|
||||
|
||||
export interface GroupMeta {
|
||||
label: string
|
||||
color: string
|
||||
label: string;
|
||||
color: string;
|
||||
/** 以活動強度遞增的圖示表達組別高低,讓組別不只靠顏色辨識 */
|
||||
icon: string
|
||||
description: string
|
||||
icon: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/** 組別的顯示資訊,文案取自 doc/課程說明.md */
|
||||
export const GROUP_META: Record<FitnessGroup, GroupMeta> = {
|
||||
low: {
|
||||
label: '低體適能',
|
||||
color: 'warning',
|
||||
icon: 'mdi-walk',
|
||||
description: '肌力、平衡與心肺功能較弱,以降低跌倒風險、重建日常生活功能為優先。',
|
||||
label: "低體適能",
|
||||
color: "warning",
|
||||
icon: "mdi-walk",
|
||||
description:
|
||||
"肌力、平衡與心肺功能較弱,以降低跌倒風險、重建日常生活功能為優先。",
|
||||
},
|
||||
medium: {
|
||||
label: '中體適能',
|
||||
color: 'info',
|
||||
icon: 'mdi-run',
|
||||
description: '尚未失能但已出現部分衰退,重點在累積功能儲備、增加肌肉量與心肺能力。',
|
||||
label: "中體適能",
|
||||
color: "info",
|
||||
icon: "mdi-run",
|
||||
description:
|
||||
"尚未失能但已出現部分衰退,重點在累積功能儲備、增加肌肉量與心肺能力。",
|
||||
},
|
||||
high: {
|
||||
label: '高體適能',
|
||||
color: 'success',
|
||||
icon: 'mdi-run-fast',
|
||||
description: '功能儲備高、跌倒風險低,目標是延緩衰退速度並長期維持既有功能。',
|
||||
label: "高體適能",
|
||||
color: "success",
|
||||
icon: "mdi-run-fast",
|
||||
description:
|
||||
"功能儲備高、跌倒風險低,目標是延緩衰退速度並長期維持既有功能。",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 三組一週課表,內容逐格轉錄自 doc/課程表.png。
|
||||
* 後端完成後這份資料會由第 5 支 API 提供。
|
||||
*/
|
||||
function createCourseTable (): Record<FitnessGroup, CourseRow[]> {
|
||||
function createCourseTable(): Record<FitnessGroup, CourseRow[]> {
|
||||
return {
|
||||
low: [
|
||||
{ day: 1, morning: '慢走 15 分鐘', noon: '正常飲食,多吃蔬菜與蛋白質', night: '坐站訓練 10 次 ×2 組+伸展 10 分鐘' },
|
||||
{ day: 2, morning: '公園散步 20 分鐘', noon: '1 顆蛋+豆漿', night: '單腳站立和平衡訓練 15 分鐘' },
|
||||
{ day: 3, morning: '慢走 15 分鐘', noon: '魚肉與青菜', night: '扶牆踮腳 15 次 ×2 組' },
|
||||
{ day: 4, morning: '休閒散步 20 分鐘', noon: '補充水果', night: '伸展運動 10 分鐘' },
|
||||
{ day: 5, morning: '慢走 20 分鐘', noon: '雞肉與蔬菜', night: '坐站訓練+平衡訓練' },
|
||||
{ day: 6, morning: '腳踏車或散步 30 分鐘', noon: '均衡飲食', night: '全身伸展' },
|
||||
{ day: 7, morning: '輕鬆散步', noon: '與家人共餐', night: '早睡休息' },
|
||||
{
|
||||
day: 1,
|
||||
morning: "慢走 15 分鐘",
|
||||
noon: "正常飲食,多吃蔬菜與蛋白質",
|
||||
night: "坐站訓練 10 次 ×2 組+伸展 10 分鐘",
|
||||
},
|
||||
{
|
||||
day: 2,
|
||||
morning: "公園散步 20 分鐘",
|
||||
noon: "1 顆蛋+豆漿",
|
||||
night: "單腳站立和平衡訓練 15 分鐘",
|
||||
},
|
||||
{
|
||||
day: 3,
|
||||
morning: "慢走 15 分鐘",
|
||||
noon: "魚肉與青菜",
|
||||
night: "扶牆踮腳 15 次 ×2 組",
|
||||
},
|
||||
{
|
||||
day: 4,
|
||||
morning: "休閒散步 20 分鐘",
|
||||
noon: "補充水果",
|
||||
night: "伸展運動 10 分鐘",
|
||||
},
|
||||
{
|
||||
day: 5,
|
||||
morning: "慢走 20 分鐘",
|
||||
noon: "雞肉與蔬菜",
|
||||
night: "坐站訓練+平衡訓練",
|
||||
},
|
||||
{
|
||||
day: 6,
|
||||
morning: "腳踏車或散步 30 分鐘",
|
||||
noon: "均衡飲食",
|
||||
night: "全身伸展",
|
||||
},
|
||||
{ day: 7, morning: "輕鬆散步", noon: "與家人共餐", night: "早睡休息" },
|
||||
],
|
||||
medium: [
|
||||
{ day: 1, morning: '快走 30 分鐘', noon: '高蛋白午餐', night: '深蹲 15 次 ×3 組' },
|
||||
{ day: 2, morning: '騎腳踏車 40 分鐘', noon: '水果與豆類', night: '伸展 15 分鐘' },
|
||||
{ day: 3, morning: '快走 30 分鐘', noon: '魚肉與蔬菜', night: '彈力帶訓練' },
|
||||
{ day: 4, morning: '游泳或太極 40 分鐘', noon: '均衡飲食', night: '核心訓練 15 分鐘' },
|
||||
{ day: 5, morning: '快走 30 分鐘', noon: '高蛋白午餐', night: '深蹲+彈力帶' },
|
||||
{ day: 6, morning: '郊山健行 60 分鐘', noon: '補充水分', night: '輕鬆散步' },
|
||||
{ day: 7, morning: '休息', noon: '家人聚餐', night: '伸展 10 分鐘' },
|
||||
{
|
||||
day: 1,
|
||||
morning: "快走 30 分鐘",
|
||||
noon: "高蛋白午餐",
|
||||
night: "深蹲 15 次 ×3 組",
|
||||
},
|
||||
{
|
||||
day: 2,
|
||||
morning: "騎腳踏車 40 分鐘",
|
||||
noon: "水果與豆類",
|
||||
night: "伸展 15 分鐘",
|
||||
},
|
||||
{
|
||||
day: 3,
|
||||
morning: "快走 30 分鐘",
|
||||
noon: "魚肉與蔬菜",
|
||||
night: "彈力帶訓練",
|
||||
},
|
||||
{
|
||||
day: 4,
|
||||
morning: "游泳或太極 40 分鐘",
|
||||
noon: "均衡飲食",
|
||||
night: "核心訓練 15 分鐘",
|
||||
},
|
||||
{
|
||||
day: 5,
|
||||
morning: "快走 30 分鐘",
|
||||
noon: "高蛋白午餐",
|
||||
night: "深蹲+彈力帶",
|
||||
},
|
||||
{
|
||||
day: 6,
|
||||
morning: "郊山健行 60 分鐘",
|
||||
noon: "補充水分",
|
||||
night: "輕鬆散步",
|
||||
},
|
||||
{ day: 7, morning: "休息", noon: "家人聚餐", night: "伸展 10 分鐘" },
|
||||
],
|
||||
high: [
|
||||
{ day: 1, morning: '重量訓練 60 分鐘', noon: '蛋白質與蔬菜', night: '散步 20 分鐘' },
|
||||
{ day: 2, morning: '快走或慢跑 45 分鐘', noon: '水果與堅果', night: '核心訓練 20 分鐘' },
|
||||
{ day: 3, morning: '平衡與敏捷訓練 40 分鐘', noon: '均衡飲食', night: '伸展 15 分鐘' },
|
||||
{ day: 4, morning: '重量訓練 60 分鐘', noon: '高蛋白午餐', night: '散步 20 分鐘' },
|
||||
{ day: 5, morning: '間歇跑 30 分鐘', noon: '補充水分', night: '輕量伸展' },
|
||||
{ day: 6, morning: '登山、羽球或游泳 90 分鐘', noon: '魚類與蔬菜', night: '放鬆休息' },
|
||||
{ day: 7, morning: '輕鬆散步 30 分鐘', noon: '家人聚餐', night: '早睡恢復' },
|
||||
{
|
||||
day: 1,
|
||||
morning: "重量訓練 60 分鐘",
|
||||
noon: "蛋白質與蔬菜",
|
||||
night: "散步 20 分鐘",
|
||||
},
|
||||
{
|
||||
day: 2,
|
||||
morning: "快走或慢跑 45 分鐘",
|
||||
noon: "水果與堅果",
|
||||
night: "核心訓練 20 分鐘",
|
||||
},
|
||||
{
|
||||
day: 3,
|
||||
morning: "平衡與敏捷訓練 40 分鐘",
|
||||
noon: "均衡飲食",
|
||||
night: "伸展 15 分鐘",
|
||||
},
|
||||
{
|
||||
day: 4,
|
||||
morning: "重量訓練 60 分鐘",
|
||||
noon: "高蛋白午餐",
|
||||
night: "散步 20 分鐘",
|
||||
},
|
||||
{
|
||||
day: 5,
|
||||
morning: "間歇跑 30 分鐘",
|
||||
noon: "補充水分",
|
||||
night: "輕量伸展",
|
||||
},
|
||||
{
|
||||
day: 6,
|
||||
morning: "登山、羽球或游泳 90 分鐘",
|
||||
noon: "魚類與蔬菜",
|
||||
night: "放鬆休息",
|
||||
},
|
||||
{
|
||||
day: 7,
|
||||
morning: "輕鬆散步 30 分鐘",
|
||||
noon: "家人聚餐",
|
||||
night: "早睡恢復",
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function delay (ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/** 模擬後端依 Y/N 篩選時段欄位的行為 */
|
||||
function mockCourseList (params: CourseListParams): CourseList {
|
||||
return createCourseTable()[params.group].map(row => {
|
||||
const filtered: CourseRow = { day: row.day }
|
||||
function mockCourseList(params: CourseListParams): CourseList {
|
||||
return createCourseTable()[params.group].map((row) => {
|
||||
const filtered: CourseRow = { day: row.day };
|
||||
|
||||
if (params.morning === 'Y') {
|
||||
filtered.morning = row.morning
|
||||
if (params.morning === "Y") {
|
||||
filtered.morning = row.morning;
|
||||
}
|
||||
if (params.noon === 'Y') {
|
||||
filtered.noon = row.noon
|
||||
if (params.noon === "Y") {
|
||||
filtered.noon = row.noon;
|
||||
}
|
||||
if (params.night === 'Y') {
|
||||
filtered.night = row.night
|
||||
if (params.night === "Y") {
|
||||
filtered.night = row.night;
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
|
||||
export const useCourseStore = defineStore('course', () => {
|
||||
const group = ref<FitnessGroup>('medium')
|
||||
const morning = ref<YesNo>('Y')
|
||||
const noon = ref<YesNo>('Y')
|
||||
const night = ref<YesNo>('Y')
|
||||
const courseList = ref<CourseList>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
export const useCourseStore = defineStore("course", () => {
|
||||
const group = ref<FitnessGroup>("medium");
|
||||
const morning = ref<YesNo>("Y");
|
||||
const noon = ref<YesNo>("Y");
|
||||
const night = ref<YesNo>("Y");
|
||||
const courseList = ref<CourseList>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
const groupMeta = computed(() => GROUP_META[group.value])
|
||||
const groupMeta = computed(() => GROUP_META[group.value]);
|
||||
|
||||
/** 目前被選取的時段,供畫面判斷要顯示哪些欄位 */
|
||||
const selectedSlots = computed<TimeSlot[]>(() => {
|
||||
const slots: TimeSlot[] = []
|
||||
const slots: TimeSlot[] = [];
|
||||
|
||||
if (morning.value === 'Y') {
|
||||
slots.push('morning')
|
||||
if (morning.value === "Y") {
|
||||
slots.push("morning");
|
||||
}
|
||||
if (noon.value === 'Y') {
|
||||
slots.push('noon')
|
||||
if (noon.value === "Y") {
|
||||
slots.push("noon");
|
||||
}
|
||||
if (night.value === 'Y') {
|
||||
slots.push('night')
|
||||
if (night.value === "Y") {
|
||||
slots.push("night");
|
||||
}
|
||||
|
||||
return slots
|
||||
})
|
||||
return slots;
|
||||
});
|
||||
|
||||
const hasCourse = computed(() => selectedSlots.value.length > 0 && courseList.value.length > 0)
|
||||
const hasCourse = computed(
|
||||
() => selectedSlots.value.length > 0 && courseList.value.length > 0,
|
||||
);
|
||||
|
||||
/** 前 4 支 API:平行取得組別與三個時段設定 */
|
||||
async function loadPreferences () {
|
||||
async function loadPreferences() {
|
||||
if (USE_MOCK) {
|
||||
await delay(MOCK_DELAY)
|
||||
group.value = 'medium'
|
||||
morning.value = 'Y'
|
||||
noon.value = 'Y'
|
||||
night.value = 'Y'
|
||||
return
|
||||
await delay(MOCK_DELAY);
|
||||
group.value = "medium";
|
||||
morning.value = "Y";
|
||||
noon.value = "Y";
|
||||
night.value = "Y";
|
||||
return;
|
||||
}
|
||||
|
||||
// 真實後端:course/group 需要 userID,course/morning、noon、night 需要 group,
|
||||
// 兩者有相依順序,因此無法四支平行呼叫(與 doc/課程建議API規格.md 的示意圖不同,
|
||||
// 以 doc/課程建議_API_前端使用說明_20260806.md 的建議測試流程為準)
|
||||
const { userID } = useAppStore()
|
||||
const groupValue = await fetchGroup(userID)
|
||||
const { userID } = useAppStore();
|
||||
const groupValue = await fetchGroup(userID);
|
||||
|
||||
const [morningValue, noonValue, nightValue] = await Promise.all([
|
||||
fetchMorning(groupValue),
|
||||
fetchNoon(groupValue),
|
||||
fetchNight(groupValue),
|
||||
])
|
||||
]);
|
||||
|
||||
group.value = groupValue
|
||||
morning.value = morningValue
|
||||
noon.value = noonValue
|
||||
night.value = nightValue
|
||||
group.value = groupValue;
|
||||
morning.value = morningValue;
|
||||
noon.value = noonValue;
|
||||
night.value = nightValue;
|
||||
}
|
||||
|
||||
/** 第 5 支 API:以前 4 個值為參數取得課表 */
|
||||
async function loadCourseList () {
|
||||
async function loadCourseList() {
|
||||
const params: CourseListParams = {
|
||||
group: group.value,
|
||||
morning: morning.value,
|
||||
noon: noon.value,
|
||||
night: night.value,
|
||||
}
|
||||
};
|
||||
|
||||
if (USE_MOCK) {
|
||||
await delay(MOCK_DELAY)
|
||||
courseList.value = mockCourseList(params)
|
||||
return
|
||||
await delay(MOCK_DELAY);
|
||||
courseList.value = mockCourseList(params);
|
||||
return;
|
||||
}
|
||||
|
||||
courseList.value = await fetchCourseList(params)
|
||||
courseList.value = await fetchCourseList(params);
|
||||
}
|
||||
|
||||
/** 頁面進入時呼叫:先取設定再取課表 */
|
||||
async function loadAll () {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
async function loadAll() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
await loadPreferences()
|
||||
await loadCourseList()
|
||||
await loadPreferences();
|
||||
await loadCourseList();
|
||||
} catch (error_) {
|
||||
error.value = error_ instanceof Error ? error_.message : '課程資料載入失敗,請稍後再試。'
|
||||
courseList.value = []
|
||||
error.value =
|
||||
error_ instanceof Error
|
||||
? error_.message
|
||||
: "課程資料載入失敗,請稍後再試。";
|
||||
courseList.value = [];
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 使用者切換時段後重新取課表 */
|
||||
async function setSlot (slot: TimeSlot, value: YesNo) {
|
||||
const target = { morning, noon, night }[slot]
|
||||
async function setSlot(slot: TimeSlot, value: YesNo) {
|
||||
const target = { morning, noon, night }[slot];
|
||||
|
||||
if (target.value === value) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
target.value = value
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
target.value = value;
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
await loadCourseList()
|
||||
await loadCourseList();
|
||||
} catch (error_) {
|
||||
error.value = error_ instanceof Error ? error_.message : '課程資料載入失敗,請稍後再試。'
|
||||
courseList.value = []
|
||||
error.value =
|
||||
error_ instanceof Error
|
||||
? error_.message
|
||||
: "課程資料載入失敗,請稍後再試。";
|
||||
courseList.value = [];
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,5 +348,5 @@ export const useCourseStore = defineStore('course', () => {
|
||||
loadCourseList,
|
||||
loadPreferences,
|
||||
setSlot,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user