551 lines
17 KiB
Vue
551 lines
17 KiB
Vue
<template>
|
||
<Layout>
|
||
<template #main>
|
||
<template v-if="store.groupLoaded">
|
||
<!-- 頁面標題:刻意縮小,把版面讓給下方的組別 -->
|
||
<div class="d-flex align-center mb-3">
|
||
<v-icon
|
||
class="mr-2"
|
||
color="primary"
|
||
icon="mdi-calendar-week"
|
||
size="18"
|
||
/>
|
||
|
||
<span class="text-title-small font-weight-bold text-grey-darken-1"
|
||
>課程建議</span
|
||
>
|
||
</div>
|
||
|
||
<!-- 體適能組別:本頁所有內容都依它而定,因此獨立成一張顯眼的卡片 -->
|
||
<v-card
|
||
class="rounded-xl mb-6 overflow-hidden"
|
||
elevation="1"
|
||
variant="flat"
|
||
>
|
||
<v-card-item class="pa-4" :class="`bg-${store.groupMeta.color}`">
|
||
<template #prepend>
|
||
<v-avatar class="group-avatar" size="60">
|
||
<v-icon color="white" :icon="store.groupMeta.icon" size="34" />
|
||
</v-avatar>
|
||
</template>
|
||
|
||
<template #subtitle>
|
||
<span class="text-title-small group-caption">你的體適能組別</span>
|
||
</template>
|
||
|
||
<template #title>
|
||
<span class="text-h5 font-weight-black text-white">{{
|
||
store.groupMeta.label
|
||
}}</span>
|
||
</template>
|
||
</v-card-item>
|
||
|
||
<v-card-text class="text-body-medium text-grey-darken-2 pa-4">
|
||
{{ store.groupMeta.description }}
|
||
</v-card-text>
|
||
</v-card>
|
||
|
||
<!-- 時段篩選尚未開放給使用者操作,先隱藏 -->
|
||
<v-card v-if="SHOW_SLOT_FILTER" class="rounded-xl mb-6" variant="flat">
|
||
<v-card-text>
|
||
<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>
|
||
<!-- 課程說明:對應 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>
|
||
|
||
<!-- 運動時間建議:對應 doc/運動時間建議.png -->
|
||
<v-card class="rounded-lg elevation-1 mt-4 overflow-hidden">
|
||
<v-expansion-panels
|
||
v-model="durationTablePanel"
|
||
flat
|
||
variant="accordion"
|
||
>
|
||
<v-expansion-panel value="duration">
|
||
<v-expansion-panel-title
|
||
class="course-table-panel-title text-title-medium font-weight-bold text-primary"
|
||
>
|
||
<v-icon
|
||
class="mr-2"
|
||
color="primary"
|
||
icon="mdi-timer-outline"
|
||
size="20"
|
||
/>
|
||
運動時間建議
|
||
</v-expansion-panel-title>
|
||
|
||
<v-divider />
|
||
|
||
<v-expansion-panel-text class="pa-0">
|
||
<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-expansion-panel-text>
|
||
</v-expansion-panel>
|
||
</v-expansion-panels>
|
||
</v-card>
|
||
|
||
<v-card class="rounded-lg elevation-1 mt-4 overflow-hidden">
|
||
<v-expansion-panels
|
||
v-model="scheduleTablePanel"
|
||
flat
|
||
variant="accordion"
|
||
>
|
||
<v-expansion-panel value="schedule">
|
||
<v-expansion-panel-title
|
||
class="course-table-panel-title text-title-medium font-weight-bold text-primary"
|
||
>
|
||
<v-icon
|
||
class="mr-2"
|
||
color="primary"
|
||
icon="mdi-timer-outline"
|
||
size="20"
|
||
/>
|
||
運動課表
|
||
</v-expansion-panel-title>
|
||
|
||
<v-divider />
|
||
|
||
<v-expansion-panel-text class="pa-0">
|
||
<v-table class="course-table" density="comfortable">
|
||
<thead>
|
||
<tr>
|
||
<th class="day-col bg-primary 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"
|
||
: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
|
||
v-for="slot in visibleSlots"
|
||
:key="slot.key"
|
||
class="slot-cell text-body-medium text-wrap"
|
||
>
|
||
<div
|
||
class="d-flex flex-column flex-md-row align-center ga-1 ga-md-3"
|
||
>
|
||
<v-avatar
|
||
class="slot-avatar flex-shrink-0"
|
||
rounded="lg"
|
||
>
|
||
<v-img
|
||
:alt="`${DAY_NAMES[row.day - 1]}${slot.label}課表插圖`"
|
||
:src="scheduleCardImage(row.day, slot.key)"
|
||
/>
|
||
</v-avatar>
|
||
|
||
<span>{{ row[slot.key] }}</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</v-table>
|
||
</v-expansion-panel-text>
|
||
</v-expansion-panel>
|
||
</v-expansion-panels>
|
||
</v-card>
|
||
</template>
|
||
</template>
|
||
|
||
<v-alert
|
||
v-else-if="store.groupMissing"
|
||
class="rounded-xl"
|
||
icon="mdi-calendar-alert"
|
||
type="info"
|
||
variant="tonal"
|
||
>
|
||
尚未取得體適能分組,請先預約並完成現場普測。
|
||
|
||
<template #append>
|
||
<v-btn to="/appointment" variant="text">前往預約</v-btn>
|
||
</template>
|
||
</v-alert>
|
||
|
||
<v-alert
|
||
v-else-if="store.groupError"
|
||
class="rounded-xl"
|
||
icon="mdi-alert-circle-outline"
|
||
type="error"
|
||
variant="tonal"
|
||
>
|
||
{{ store.groupError }}
|
||
|
||
<template #append>
|
||
<v-btn size="small" variant="text" @click="store.loadGroup(true)"
|
||
>重試</v-btn
|
||
>
|
||
</template>
|
||
</v-alert>
|
||
|
||
<v-skeleton-loader v-else class="rounded-xl" type="article" />
|
||
</template>
|
||
</Layout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { TimeSlot, YesNo } from "@/api/types";
|
||
import { computed, onMounted, ref } from "vue";
|
||
import Layout from "@/components/layout/DefaultLayout.vue";
|
||
import { COURSE_DETAIL } from "@/data/courseDetail";
|
||
import { useCourseStore } from "@/stores/course";
|
||
|
||
const store = useCourseStore();
|
||
|
||
// 運動時間建議與課程說明的靜態內容,依組別對應
|
||
const detail = computed(() => COURSE_DETAIL[store.group]);
|
||
|
||
// 課程說明預設只展開第一個面板(族群特徵)
|
||
const openPanels = ref([0]);
|
||
|
||
// 兩張表格預設展開,使用者可分別收合以減少頁面長度。
|
||
const durationTablePanel = ref<string | null>("duration");
|
||
const scheduleTablePanel = ref<string | null>("schedule");
|
||
|
||
// 時段篩選功能尚未開放,UI 先隱藏;之後要開放時把這個常數改回 true 即可
|
||
const SHOW_SLOT_FILTER = false;
|
||
|
||
const DAY_NAMES = ["一", "二", "三", "四", "五", "六", "日"];
|
||
|
||
// 課表小卡:檔名為「星期-時段」,逐一匯入 21 張圖並依 day/slot 查表取用
|
||
const scheduleCardModules = import.meta.glob<{ default: string }>(
|
||
"../assets/course/schedule/*.png",
|
||
{ eager: true },
|
||
);
|
||
|
||
const SCHEDULE_CARD_DAY_KEYS = [
|
||
"monday",
|
||
"tuesday",
|
||
"wednesday",
|
||
"thursday",
|
||
"friday",
|
||
"saturday",
|
||
"sunday",
|
||
];
|
||
const SCHEDULE_CARD_SLOT_KEYS: Record<TimeSlot, string> = {
|
||
morning: "morning",
|
||
noon: "noon",
|
||
night: "evening",
|
||
};
|
||
|
||
const SCHEDULE_CARDS: Record<string, string> = Object.fromEntries(
|
||
Object.entries(scheduleCardModules).map(([path, module]) => [
|
||
path.replace(/^.*\/|\.png$/g, ""),
|
||
module.default,
|
||
]),
|
||
);
|
||
|
||
function scheduleCardImage(day: number, slot: TimeSlot): string {
|
||
const key = `${SCHEDULE_CARD_DAY_KEYS[day - 1]}-${SCHEDULE_CARD_SLOT_KEYS[slot]}`;
|
||
return SCHEDULE_CARDS[key];
|
||
}
|
||
|
||
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)),
|
||
);
|
||
|
||
// 今天對應的星期幾,換算成與 CourseRow.day 相同的規則(1 = 週一 … 7 = 週日)
|
||
const todayDay = computed(() => {
|
||
const jsDay = new Date().getDay();
|
||
return jsDay === 0 ? 7 : jsDay;
|
||
});
|
||
|
||
// 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;
|
||
}
|
||
|
||
.course-table-panel-title {
|
||
min-height: 40px;
|
||
padding: 8px 16px;
|
||
}
|
||
|
||
.day-col {
|
||
width: 15%;
|
||
}
|
||
|
||
.time-col {
|
||
width: 22%;
|
||
}
|
||
|
||
/* 課表小卡:欄寬有限,圖片改用 clamp 隨欄寬縮放,避免在手機版把文字擠出可讀範圍 */
|
||
.slot-cell {
|
||
padding-block: 8px;
|
||
}
|
||
|
||
.slot-avatar {
|
||
width: clamp(32px, 18vw, 56px);
|
||
height: clamp(32px, 18vw, 56px);
|
||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
/* 組別卡片:半透明白底圓形與次級白字,Vuetify 沒有對應的 utility class */
|
||
.group-avatar {
|
||
background-color: rgba(255, 255, 255, 0.22);
|
||
}
|
||
|
||
.group-caption {
|
||
color: rgba(255, 255, 255, 0.85);
|
||
}
|
||
|
||
.pa-0 :deep(.v-expansion-panel-text__wrapper) {
|
||
padding: 0;
|
||
}
|
||
</style>
|