feat: 課程建議

This commit is contained in:
skytek_xinliang
2026-08-05 15:45:45 +08:00
parent c9c7363dc8
commit 889e44f68e
13 changed files with 538 additions and 104 deletions
+170
View File
@@ -0,0 +1,170 @@
<template>
<Layout>
<template #main>
<!-- 標題卡:組別與說明 -->
<v-card class="border-primary border-xl rounded-xl mb-6" variant="flat">
<v-card-title class="d-flex align-center">
<v-icon class="mr-2" color="primary" icon="mdi-calendar-week" />
課程建議
<v-spacer />
<v-chip :color="store.groupMeta.color" size="small" variant="flat">
{{ store.groupMeta.label }}
</v-chip>
</v-card-title>
<v-card-subtitle class="text-wrap">
{{ store.groupMeta.description }}
</v-card-subtitle>
<v-card-text class="pt-4">
<div class="text-title-small text-grey mb-2">選擇要顯示的時段</div>
<v-btn-toggle
v-model="selectedSlots"
border
class="d-flex"
color="primary"
divided
multiple
variant="flat"
>
<v-btn
v-for="slot in SLOTS"
:key="slot.key"
class="flex-grow-1"
:prepend-icon="slot.icon"
size="small"
:value="slot.key"
>
{{ slot.label }}
</v-btn>
</v-btn-toggle>
</v-card-text>
</v-card>
<!-- 載入中 -->
<template v-if="store.loading">
<v-skeleton-loader
v-for="n in 3"
:key="n"
class="rounded-xl mb-4"
type="article"
/>
</template>
<!-- 載入失敗 -->
<v-alert
v-else-if="store.error"
class="rounded-xl"
icon="mdi-alert-circle-outline"
type="error"
variant="tonal"
>
{{ store.error }}
<template #append>
<v-btn size="small" variant="text" @click="store.loadAll()">重試</v-btn>
</template>
</v-alert>
<!-- 沒有選取任何時段 -->
<v-alert
v-else-if="!store.hasCourse"
class="rounded-xl"
icon="mdi-calendar-remove-outline"
type="info"
variant="tonal"
>
目前沒有可顯示的課程,請至少選擇一個時段。
</v-alert>
<!-- 一週課表:仿 doc/課程表示意 的表格版型,星期為列、時段為欄 -->
<template v-else>
<v-card class="rounded-xl elevation-1 overflow-hidden">
<v-table class="course-table" density="comfortable">
<thead>
<tr>
<th class="day-col bg-secondary text-white text-center">星期</th>
<th
v-for="slot in visibleSlots"
:key="slot.key"
class="text-white text-center"
:class="slot.headerClass"
>
<v-icon class="mr-1" :icon="slot.icon" size="16" />
{{ slot.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in store.courseList" :key="row.day">
<td class="day-col text-center font-weight-bold text-primary">
{{ DAY_NAMES[row.day - 1] }}
</td>
<td
v-for="slot in visibleSlots"
:key="slot.key"
class="text-body-medium text-wrap"
>
{{ row[slot.key] }}
</td>
</tr>
</tbody>
</v-table>
</v-card>
</template>
</template>
</Layout>
</template>
<script setup lang="ts">
import type { TimeSlot, YesNo } from '@/api/types'
import { computed, onMounted } from 'vue'
import Layout from '@/components/layout/DefaultLayout.vue'
import { useCourseStore } from '@/stores/course'
const store = useCourseStore()
const DAY_NAMES = ['一', '二', '三', '四', '五', '六', '日']
const SLOTS: { key: TimeSlot, label: string, icon: string, headerClass: string }[] = [
{ key: 'morning', label: '早上', icon: 'mdi-weather-sunset-up', headerClass: 'bg-deep-orange' },
{ key: 'noon', label: '中午', icon: 'mdi-white-balance-sunny', headerClass: 'bg-amber-darken-2' },
{ key: 'night', label: '晚上', icon: 'mdi-weather-night', headerClass: 'bg-deep-purple' },
]
// 依目前選取的時段決定表格要顯示哪幾欄,順序固定為早上/中午/晚上
const visibleSlots = computed(() => SLOTS.filter(slot => store.selectedSlots.includes(slot.key)))
// v-btn-toggle 以陣列表達勾選狀態,store 則以 Y/N 保存,兩者在此橋接
const selectedSlots = computed<TimeSlot[]>({
get: () => store.selectedSlots,
set: value => {
for (const slot of SLOTS) {
const next: YesNo = value.includes(slot.key) ? 'Y' : 'N'
store.setSlot(slot.key, next)
}
},
})
onMounted(() => {
store.loadAll()
})
</script>
<style scoped>
/* Vuetify 的 v-table 沒有「欄位依內容自動收窄/固定星期欄寬」的內建設定,
需要 table-layout: fixed 才能讓中文內容改為換行而非撐開表格。 */
.course-table :deep(table) {
table-layout: fixed;
}
.day-col {
width: 15%;
}
</style>
+9 -97
View File
@@ -54,18 +54,18 @@
</v-col>
</v-row>
<!-- 功能儲備與運動處方簡介 -->
<!-- 課程建議入口 -->
<v-card-text class="bg-primary">
<div class="font-weight-bold text-accent-light">
<v-list-item append-icon="mdi-chevron-right" class="bg-primary py-4" to="/course">
<v-list-item-title class="font-weight-bold text-accent-light">
<v-icon icon="mdi-notebook-check-outline" size="16" />
運動處方建議:
</div>
課程建議
</v-list-item-title>
<p class="text-white mt-2">
{{ currentWeeklyPrescription }}
</p>
</v-card-text>
<v-list-item-subtitle class="text-white mt-2">
依你的體適能組別,查看一週的每日課表
</v-list-item-subtitle>
</v-list-item>
</v-card>
<!-- 中間:功能指標與五維雷達圖 -->
@@ -110,82 +110,6 @@
title="預約"
to="/appointment"
/>
<!-- 運動處方對話框 -->
<v-dialog v-model="prescriptionDialog" max-width="500">
<v-card class="rounded-xl">
<v-card-title class="bg-primary text-white font-weight-bold d-flex align-center">
<v-icon class="mr-2" icon="mdi-dumbbell" />
本週運動處方明細
</v-card-title>
<v-card-text class="pt-4 text-body-1">
<h4 class="font-weight-bold text-primary mb-2">每週訓練任務進度</h4>
<div class="mb-4">
<div class="d-flex justify-space-between align-center text-caption mb-1">
<span>任務完成度 (2/7堂課)</span>
<span>28%</span>
</div>
<v-progress-linear color="secondary" height="6" model-value="28" rounded />
</div>
<!-- 動作說明清單 -->
<v-list class="pa-0" density="comfortable">
<v-list-item class="bg-teal-lighten-5 rounded-lg mb-2 py-2">
<template #prepend>
<v-icon color="secondary" icon="mdi-numeric-1-circle" size="24" />
</template>
<v-list-item-title class="font-weight-bold">下肢肌力:坐站起立訓練</v-list-item-title>
<v-list-item-subtitle class="text-caption mt-1">
每週 2 次,每次 3 組,每組 10-12 次。<br />
<span class="text-error font-weight-bold">* 扶手保護,站起時吐氣。</span>
</v-list-item-subtitle>
</v-list-item>
<v-list-item class="bg-teal-lighten-5 rounded-lg mb-2 py-2">
<template #prepend>
<v-icon color="secondary" icon="mdi-numeric-2-circle" size="24" />
</template>
<v-list-item-title class="font-weight-bold">靜態平衡:扶椅單腳站立</v-list-item-title>
<v-list-item-subtitle class="text-caption mt-1">
每週 3 次,左右腳各 30 秒,重複 3 次。<br />
<span class="text-grey-darken-1">手輕扶椅背,確保安全。</span>
</v-list-item-subtitle>
</v-list-item>
<v-list-item class="bg-teal-lighten-5 rounded-lg py-2">
<template #prepend>
<v-icon color="secondary" icon="mdi-numeric-3-circle" size="24" />
</template>
<v-list-item-title class="font-weight-bold">心肺耐力:每日快走</v-list-item-title>
<v-list-item-subtitle class="text-caption mt-1">
每日 20-30 分鐘,達到微喘但仍可說話的強度。<br />
<span class="text-error font-weight-bold">* 若感覺頭暈或膝蓋疼痛請暫停!</span>
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions class="justify-end px-4 pb-4">
<v-btn
class="rounded-lg px-4"
color="primary"
variant="flat"
@click="prescriptionDialog = false"
>
確認關閉
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<template v-else>
@@ -268,7 +192,6 @@ import ModuleRouterCard from '@/components/ModuleRouterCard.vue'
import { useAppStore } from '@/stores/app'
const store = useAppStore()
const prescriptionDialog = ref(false)
const displayedHealthScore = ref(1)
let healthScoreTimer: number | undefined
@@ -325,17 +248,6 @@ const todayTimeString = computed(() => {
return `${date} ${time}`
})
// 每週處方文字
const currentWeeklyPrescription = computed(() => {
if (store.healthScore >= 80) {
return '1次肌力循環 + 1次平衡課 + 每日20分鐘快走'
} else if (store.healthScore >= 65) {
return '2次肌力循環 + 1次平衡課 + 每日20分鐘快走'
} else {
return '3次防跌平衡引導 + 每日15分鐘扶椅坐站與慢走'
}
})
</script>
<style scoped>