feat: 課程表格

This commit is contained in:
skytek_xinliang
2026-08-05 17:25:45 +08:00
parent 76aa074506
commit 7938903ada
3 changed files with 267 additions and 2 deletions
@@ -0,0 +1,112 @@
{
"low": {
"duration": [
{ "day": 1, "content": "快走+下肢肌力", "minutes": 40 },
{ "day": 2, "content": "平衡訓練+伸展", "minutes": 30 },
{ "day": 3, "content": "快走+上肢肌力", "minutes": 40 },
{ "day": 4, "content": "休息或散步", "minutes": 20 },
{ "day": 5, "content": "肌力+平衡", "minutes": 40 },
{ "day": 6, "content": "有氧運動", "minutes": 40 },
{ "day": 7, "content": "休息", "minutes": null }
],
"profile": {
"summary": "根據研究,平衡、敏捷與肌肉品質是老化的預測指標。40 歲後肌力與神經控制能力每年流失約 1–2%,而平衡能力下降是跌倒的重要預測因子。",
"items": [
"肌力、平衡與心肺功能均較差。",
"跌倒風險高。",
"容易出現肌少症與脂肪浸潤。",
"日常生活功能開始下降。"
]
},
"prescription": [
{
"title": "有氧(2030 分鐘)",
"detail": "快走。室內腳踏車。水中走路。強度:最大心率 50–60%。能正常講話但略喘。"
},
{
"title": "肌力",
"detail": "每個動作:1–2 組。10–12 次。坐站訓練。靠牆伏地挺身。彈力帶划船。抬腳。階梯上下。"
},
{
"title": "平衡(1015 分鐘)",
"detail": "單腳站立。走直線。腳跟碰腳尖。"
}
],
"outcome": {
"label": "解決問題",
"items": ["肌肉流失。", "平衡能力下降。", "心肺儲備不足。", "跌倒風險升高。"]
}
},
"medium": {
"duration": [
{ "day": 1, "content": "重量訓練", "minutes": 60 },
{ "day": 2, "content": "慢跑+伸展", "minutes": 45 },
{ "day": 3, "content": "核心訓練", "minutes": 45 },
{ "day": 4, "content": "間歇有氧", "minutes": 40 },
{ "day": 5, "content": "重量訓練", "minutes": 60 },
{ "day": 6, "content": "球類或登山", "minutes": 60 },
{ "day": 7, "content": "休息", "minutes": null }
],
"profile": {
"summary": "根據功能儲備理論,中年族群常因日常生活僅使用約 30% 的生理能力,忽略功能退化。研究強調,中年時期若沒有累積足夠肌力,老年可能出現「斷崖式功能崩潰」。",
"items": ["尚未失能。", "已出現部分衰退。", "屬於「衰弱前期」。"]
},
"prescription": [
{
"title": "有氧(150300 分鐘)",
"detail": "慢跑。游泳。羽球。登山。自行車。強度:最大心率 60–75%。"
},
{
"title": "肌力",
"detail": "每個動作:2–3 組。8–12 次。深蹲。弓箭步。啞鈴推舉。划船。核心棒式。"
},
{
"title": "爆發力與敏捷(12 次)",
"detail": "快速起立。側向移動。腳跟碰腳尖。障礙步行。階梯訓練。"
}
],
"outcome": {
"label": "任務",
"items": [
"建立「功能儲備」。",
"增加肌肉量。",
"維持柔軟度。",
"降低體脂。",
"提升心肺能力。"
]
}
},
"high": {
"duration": [
{ "day": 1, "content": "重量訓練", "minutes": 70 },
{ "day": 2, "content": "間歇跑", "minutes": 50 },
{ "day": 3, "content": "核心+平衡", "minutes": 45 },
{ "day": 4, "content": "重量訓練", "minutes": 70 },
{ "day": 5, "content": "HIIT", "minutes": 30 },
{ "day": 6, "content": "球類/戶外", "minutes": 90 },
{ "day": 7, "content": "主動恢復", "minutes": 40 }
],
"profile": {
"summary": "根據研究,老化並不會停止,只能透過長期運動與良好飲食延後功能下降。",
"items": ["功能儲備高。", "肌力與平衡佳。", "跌倒風險低。", "屬於成功老化族群。"]
},
"prescription": [
{
"title": "肌力",
"detail": "每個動作:3–4 組。610 次。強度:70–85% 1RM。坐站訓練。靠牆伏地挺身。彈力帶划船。抬腳。階梯上下。"
},
{
"title": "心肺耐力(300 分鐘以上)",
"detail": "間歇跑。游泳。羽球。登山。"
},
{
"title": "神經肌肉訓練",
"detail": "每週 2 次。敏捷梯。單腳跳。反應訓練。協調訓練。"
}
],
"outcome": {
"label": "目標",
"items": ["延緩衰退速度。", "老化仍不可避免,因此維持功能儲備是關鍵。"]
}
}
}
@@ -0,0 +1,34 @@
import type { FitnessGroup } from '@/api/types'
import courseDetail from './course-detail.json'
/**
* 各體適能組別的靜態內容,資料來源為 course-detail.json。
* 內容取自 doc/運動時間建議.png 與 doc/課程說明.md
* 日後只需修改 JSON 檔並重新 build 即可更新,不必動這支或頁面。
*
* 註:檔名刻意與 course-detail.json 不同 —— vite.config.mts 的
* resolve.extensions 把 '.json' 排在 '.ts' 前面,同名時 import 會解析到 JSON。
*/
/** 一天的運動時間建議;minutes 為 null 代表原表時間欄留空(休息日) */
export interface DurationRow {
day: number
content: string
minutes: number | null
}
/** 精準運動處方建議的單一項目 */
export interface PrescriptionItem {
title: string
detail: string
}
export interface CourseDetail {
duration: DurationRow[]
profile: { summary: string, items: string[] }
prescription: PrescriptionItem[]
/** label 三組各異:低=解決問題、中=任務、高=目標 */
outcome: { label: string, items: string[] }
}
export const COURSE_DETAIL = courseDetail as Record<FitnessGroup, CourseDetail>
+121 -2
View File
@@ -78,7 +78,7 @@
<!-- 一週課表仿 doc/課程表示意 的表格版型星期為列時段為欄 --> <!-- 一週課表仿 doc/課程表示意 的表格版型星期為列時段為欄 -->
<template v-else> <template v-else>
<v-card class="rounded-xl elevation-1 mb-4 overflow-hidden"> <v-card class="rounded-lg elevation-1 mb-4 overflow-hidden">
<v-img :alt="`${store.groupMeta.label}課表示意圖`" :src="groupIllustration" /> <v-img :alt="`${store.groupMeta.label}課表示意圖`" :src="groupIllustration" />
</v-card> </v-card>
@@ -117,6 +117,114 @@
</tbody> </tbody>
</v-table> </v-table>
</v-card> </v-card>
<!-- 運動時間建議對應 doc/運動時間建議.png -->
<v-card class="rounded-lg elevation-1 mt-4 overflow-hidden">
<v-card-title class="text-title-medium font-weight-bold text-primary d-flex align-center">
<v-icon class="mr-2" color="primary" icon="mdi-timer-outline" size="20" />
運動時間建議
</v-card-title>
<v-divider />
<v-table class="course-table" density="comfortable">
<thead>
<tr>
<th class="day-col bg-primary text-white text-center">星期</th>
<th class="bg-primary text-white text-center">運動內容</th>
<th class="time-col bg-primary text-white text-center">時間</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in detail.duration"
:key="row.day"
:class="{ 'bg-teal-lighten-5': row.day === todayDay }"
>
<td class="day-col text-center font-weight-bold text-primary">
{{ DAY_NAMES[row.day - 1] }}
</td>
<td class="text-body-medium text-wrap">{{ row.content }}</td>
<td class="time-col text-center text-body-medium">
{{ row.minutes === null ? '—' : `${row.minutes}` }}
</td>
</tr>
</tbody>
</v-table>
</v-card>
<!-- 課程說明對應 doc/課程說明.md -->
<v-card class="rounded-lg elevation-1 mt-4 overflow-hidden">
<v-card-title class="text-title-medium font-weight-bold text-primary d-flex align-center">
<v-icon class="mr-2" color="primary" icon="mdi-book-open-variant" size="20" />
課程說明
</v-card-title>
<v-divider />
<v-expansion-panels v-model="openPanels" multiple variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title class="font-weight-bold">族群特徵</v-expansion-panel-title>
<v-expansion-panel-text>
<p class="text-body-medium text-grey-darken-2 mb-3">
{{ detail.profile.summary }}
</p>
<div
v-for="(item, index) in detail.profile.items"
:key="index"
class="d-flex align-start ga-3 mb-2"
>
<v-icon color="secondary" :icon="`mdi-numeric-${index + 1}-circle`" size="20" />
<div class="text-body-medium">{{ item }}</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-title class="font-weight-bold">
精準運動處方建議
</v-expansion-panel-title>
<v-expansion-panel-text>
<div
v-for="(item, index) in detail.prescription"
:key="index"
class="d-flex align-start ga-3 mb-4"
>
<v-icon color="secondary" :icon="`mdi-numeric-${index + 1}-circle`" size="20" />
<div>
<div class="font-weight-bold">{{ item.title }}</div>
<div class="text-body-medium text-grey-darken-2 mt-1">{{ item.detail }}</div>
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-title class="font-weight-bold">
{{ detail.outcome.label }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<div
v-for="(item, index) in detail.outcome.items"
:key="index"
class="d-flex align-start ga-3 mb-2"
>
<v-icon color="secondary" :icon="`mdi-numeric-${index + 1}-circle`" size="20" />
<div class="text-body-medium">{{ item }}</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card>
</template> </template>
</template> </template>
</Layout> </Layout>
@@ -124,15 +232,22 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FitnessGroup, TimeSlot, YesNo } from '@/api/types' import type { FitnessGroup, TimeSlot, YesNo } from '@/api/types'
import { computed, onMounted } from 'vue' import { computed, onMounted, ref } from 'vue'
import highIllustration from '@/assets/course/high.png' import highIllustration from '@/assets/course/high.png'
import lowIllustration from '@/assets/course/low.png' import lowIllustration from '@/assets/course/low.png'
import mediumIllustration from '@/assets/course/medium.png' import mediumIllustration from '@/assets/course/medium.png'
import Layout from '@/components/layout/DefaultLayout.vue' import Layout from '@/components/layout/DefaultLayout.vue'
import { COURSE_DETAIL } from '@/data/courseDetail'
import { useCourseStore } from '@/stores/course' import { useCourseStore } from '@/stores/course'
const store = useCourseStore() const store = useCourseStore()
// 運動時間建議與課程說明的靜態內容,依組別對應
const detail = computed(() => COURSE_DETAIL[store.group])
// 課程說明預設只展開第一個面板(族群特徵)
const openPanels = ref([0])
const GROUP_ILLUSTRATIONS: Record<FitnessGroup, string> = { const GROUP_ILLUSTRATIONS: Record<FitnessGroup, string> = {
low: lowIllustration, low: lowIllustration,
medium: mediumIllustration, medium: mediumIllustration,
@@ -187,4 +302,8 @@ onMounted(() => {
.day-col { .day-col {
width: 15%; width: 15%;
} }
.time-col {
width: 22%;
}
</style> </style>