import type { CourseList, CourseListParams, CourseRow, FitnessGroup, TimeSlot, YesNo, } from "@/api/types"; import { defineStore } from "pinia"; import { computed, ref } from "vue"; import { fetchCourseList, fetchGroup, fetchMorning, fetchNight, fetchNoon, } from "@/api/course"; /** 後端尚未完成,預設走前端模擬資料;設定 VITE_USE_MOCK=false 即改打真實 API */ const USE_MOCK = import.meta.env.VITE_USE_MOCK !== "false"; /** 模擬網路延遲,讓載入狀態在開發時可被觀察到 */ const MOCK_DELAY = 300; export interface GroupMeta { label: string; color: string; /** 以活動強度遞增的圖示表達組別高低,讓組別不只靠顏色辨識 */ icon: string; description: string; } /** 組別的顯示資訊,文案取自 doc/課程說明.md */ export const GROUP_META: Record = { low: { label: "低體適能", color: "warning", icon: "mdi-walk", description: "肌力、平衡與心肺功能較弱,以降低跌倒風險、重建日常生活功能為優先。", }, medium: { label: "中體適能", color: "info", icon: "mdi-run", description: "尚未失能但已出現部分衰退,重點在累積功能儲備、增加肌肉量與心肺能力。", }, high: { label: "高體適能", color: "success", icon: "mdi-run-fast", description: "功能儲備高、跌倒風險低,目標是延緩衰退速度並長期維持既有功能。", }, }; /** * 三組一週課表,內容逐格轉錄自 doc/課程表.png。 * 後端完成後這份資料會由第 5 支 API 提供。 */ function createCourseTable(): Record { 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: "早睡休息" }, ], 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 分鐘" }, ], 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: "早睡恢復", }, ], }; } 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 }; if (params.morning === "Y") { filtered.morning = row.morning; } if (params.noon === "Y") { filtered.noon = row.noon; } if (params.night === "Y") { filtered.night = row.night; } return filtered; }); } export const useCourseStore = defineStore("course", () => { const group = ref("medium"); const morning = ref("Y"); const noon = ref("Y"); const night = ref("Y"); const courseList = ref([]); const loading = ref(false); const error = ref(""); const groupMeta = computed(() => GROUP_META[group.value]); /** 目前被選取的時段,供畫面判斷要顯示哪些欄位 */ const selectedSlots = computed(() => { const slots: TimeSlot[] = []; if (morning.value === "Y") { slots.push("morning"); } if (noon.value === "Y") { slots.push("noon"); } if (night.value === "Y") { slots.push("night"); } return slots; }); const hasCourse = computed( () => selectedSlots.value.length > 0 && courseList.value.length > 0, ); /** 前 4 支 API:平行取得組別與三個時段設定 */ async function loadPreferences() { if (USE_MOCK) { await delay(MOCK_DELAY); group.value = "medium"; morning.value = "Y"; noon.value = "Y"; night.value = "Y"; return; } // 真實後端:course/group 由登入 Cookie 識別使用者; // course/morning、noon、night 需要 group,兩者有相依順序。 const groupValue = await fetchGroup(); 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; } /** 第 5 支 API:以前 4 個值為參數取得課表 */ 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; } courseList.value = await fetchCourseList(params); } /** 頁面進入時呼叫:先取設定再取課表 */ async function loadAll() { loading.value = true; error.value = ""; try { await loadPreferences(); await loadCourseList(); } catch (error_) { error.value = error_ instanceof Error ? error_.message : "課程資料載入失敗,請稍後再試。"; courseList.value = []; } finally { loading.value = false; } } /** 使用者切換時段後重新取課表 */ async function setSlot(slot: TimeSlot, value: YesNo) { const target = { morning, noon, night }[slot]; if (target.value === value) { return; } target.value = value; loading.value = true; error.value = ""; try { await loadCourseList(); } catch (error_) { error.value = error_ instanceof Error ? error_.message : "課程資料載入失敗,請稍後再試。"; courseList.value = []; } finally { loading.value = false; } } return { group, morning, noon, night, courseList, loading, error, groupMeta, selectedSlots, hasCourse, loadAll, loadCourseList, loadPreferences, setSlot, }; });