Files
sports-empower-hub/app/citizen-frontend/src/stores/course.ts
T
skytek_xinliang 332d00db75 feat: 7.2
2026-08-17 11:26:13 +08:00

350 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<FitnessGroup, GroupMeta> = {
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<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: "早睡休息" },
],
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<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 selectedSlots = computed<TimeSlot[]>(() => {
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,
};
});