feat: 分數改為分組

This commit is contained in:
skytek_xinliang
2026-08-31 10:04:09 +08:00
parent 28cda7be1c
commit 12519f7809
3 changed files with 101 additions and 65 deletions
+54 -55
View File
@@ -15,16 +15,47 @@
</v-col> </v-col>
<v-col class="d-flex justify-center" cols="4"> <v-col class="d-flex justify-center" cols="4">
<!-- 健康力分數大圓盤 --> <!-- 體適能分組徽章 -->
<div <div
class="score-disk d-flex flex-column justify-center align-center" :class="[
'score-disk',
'd-flex',
'flex-column',
'justify-center',
'align-center',
courseStore.groupLoaded
? `bg-${courseStore.groupMeta.color}`
: 'bg-primary',
]"
> >
<span class="score-val">{{ displayedHealthScore }}</span> <v-progress-circular
<span class="score-unit">分</span> v-if="courseStore.groupLoading"
color="white"
indeterminate
size="28"
width="3"
/>
<span v-else-if="courseStore.groupLoaded" class="group-label">
{{ courseStore.groupMeta.label }}
</span>
<span v-else class="group-label">無資料</span>
</div> </div>
</v-col> </v-col>
</v-row> </v-row>
<v-card-subtitle v-if="courseStore.groupError" class="text-error">
{{ courseStore.groupError }}
<v-btn
class="ml-2"
color="error"
size="small"
variant="text"
@click="loadFitnessGroup"
>
重試
</v-btn>
</v-card-subtitle>
<!-- 四項核心能力數值簡覽 --> <!-- 四項核心能力數值簡覽 -->
<v-row class="mt-4" no-gutters> <v-row class="mt-4" no-gutters>
<v-col class="text-center border pa-2" cols="3"> <v-col class="text-center border pa-2" cols="3">
@@ -201,59 +232,31 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { computed, watch } from "vue";
import BrandAvatar from "@/components/BrandAvatar.vue"; import BrandAvatar from "@/components/BrandAvatar.vue";
import HealthRadarChart from "@/components/HealthRadarChart.vue"; import HealthRadarChart from "@/components/HealthRadarChart.vue";
import Layout from "@/components/layout/DefaultLayout.vue"; import Layout from "@/components/layout/DefaultLayout.vue";
import ModuleRouterCard from "@/components/ModuleRouterCard.vue"; import ModuleRouterCard from "@/components/ModuleRouterCard.vue";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import { useCourseStore } from "@/stores/course";
const store = useAppStore(); const store = useAppStore();
const displayedHealthScore = ref(1); const courseStore = useCourseStore();
let healthScoreTimer: number | undefined;
function animateHealthScore(targetScore: number) { async function loadFitnessGroup() {
if (healthScoreTimer !== undefined) { if (!store.isLoggedIn) {
window.clearInterval(healthScoreTimer); courseStore.resetGroup();
}
if (targetScore <= 1) {
displayedHealthScore.value = targetScore;
return; return;
} }
displayedHealthScore.value = 1; try {
await courseStore.loadGroup();
const duration = 900; } catch {
const stepDuration = Math.max(12, Math.floor(duration / targetScore)); // 錯誤狀態由 course store 提供給畫面顯示與重試。
}
healthScoreTimer = window.setInterval(() => {
if (displayedHealthScore.value >= targetScore) {
window.clearInterval(healthScoreTimer);
healthScoreTimer = undefined;
return;
} }
displayedHealthScore.value += 1; watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true });
}, stepDuration);
}
onMounted(() => {
animateHealthScore(store.healthScore);
});
onBeforeUnmount(() => {
if (healthScoreTimer !== undefined) {
window.clearInterval(healthScoreTimer);
}
});
watch(
() => store.healthScore,
(score) => {
animateHealthScore(score);
},
);
const todayTimeString = computed(() => { const todayTimeString = computed(() => {
const now = new Date(); const now = new Date();
@@ -314,26 +317,22 @@ const todayTimeString = computed(() => {
} }
.score-disk { .score-disk {
background: radial-gradient(circle, #157ba9 0%, #13967f 100%);
border: 3px solid #ffffff; border: 3px solid #ffffff;
width: 72px; width: min(100%, 108px);
height: 72px; height: 72px;
border-radius: 50%; padding: 0 10px;
border-radius: 22px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
flex-shrink: 0; flex-shrink: 0;
} }
.score-val { .group-label {
font-size: 1.75rem; font-size: clamp(0.95rem, 3vw, 1.1rem);
font-weight: 900; font-weight: 900;
line-height: 1; line-height: 1;
color: #ffffff; color: #ffffff;
} white-space: nowrap;
.score-unit {
font-size: 0.65rem;
font-weight: bold;
color: rgba(255, 255, 255, 0.8);
} }
.transition-card { .transition-card {
-7
View File
@@ -327,7 +327,6 @@ export const useAppStore = defineStore("app", () => {
const measurements = ref<Record<string, MeasurementStation>>( const measurements = ref<Record<string, MeasurementStation>>(
createDefaultMeasurements(), createDefaultMeasurements(),
); );
const healthScore = ref(72);
const radarData = ref(createDefaultRadarData()); const radarData = ref(createDefaultRadarData());
const isReportGenerated = ref(false); const isReportGenerated = ref(false);
@@ -674,10 +673,6 @@ export const useAppStore = defineStore("app", () => {
cardio: cardScore, cardio: cardScore,
}; };
const avg = Math.round(
(strScore + balScore + flexScore + agiScore + cardScore) / 5,
);
healthScore.value = Math.min(100, avg + 5);
isReportGenerated.value = true; isReportGenerated.value = true;
} }
@@ -693,7 +688,6 @@ export const useAppStore = defineStore("app", () => {
} }
isReportGenerated.value = false; isReportGenerated.value = false;
healthScore.value = 72;
radarData.value = createDefaultRadarData(); radarData.value = createDefaultRadarData();
} }
@@ -705,7 +699,6 @@ export const useAppStore = defineStore("app", () => {
selfAssessment, selfAssessment,
appointments, appointments,
measurements, measurements,
healthScore,
radarData, radarData,
isReportGenerated, isReportGenerated,
completedStationsCount, completedStationsCount,
+47 -3
View File
@@ -209,8 +209,15 @@ function mockCourseList(params: CourseListParams): CourseList {
}); });
} }
function isFitnessGroup(value: unknown): value is FitnessGroup {
return value === "low" || value === "medium" || value === "high";
}
export const useCourseStore = defineStore("course", () => { export const useCourseStore = defineStore("course", () => {
const group = ref<FitnessGroup>("medium"); const group = ref<FitnessGroup>("medium");
const groupLoaded = ref(false);
const groupLoading = ref(false);
const groupError = ref("");
const morning = ref<YesNo>("Y"); const morning = ref<YesNo>("Y");
const noon = ref<YesNo>("Y"); const noon = ref<YesNo>("Y");
const night = ref<YesNo>("Y"); const night = ref<YesNo>("Y");
@@ -241,11 +248,43 @@ export const useCourseStore = defineStore("course", () => {
() => selectedSlots.value.length > 0 && courseList.value.length > 0, () => selectedSlots.value.length > 0 && courseList.value.length > 0,
); );
/** 只取得目前登入使用者的體適能組別,供首頁摘要使用。 */
async function loadGroup(): Promise<FitnessGroup> {
groupLoading.value = true;
groupError.value = "";
try {
const groupValue = USE_MOCK ? "medium" : await fetchGroup();
if (!isFitnessGroup(groupValue)) {
throw new Error("體適能組別資料格式不正確。");
}
group.value = groupValue;
groupLoaded.value = true;
return groupValue;
} catch (error_) {
groupLoaded.value = false;
groupError.value =
error_ instanceof Error
? error_.message
: "體適能組別載入失敗,請稍後再試。";
throw error_;
} finally {
groupLoading.value = false;
}
}
function resetGroup() {
group.value = "medium";
groupLoaded.value = false;
groupError.value = "";
}
/** 前 4 支 API:平行取得組別與三個時段設定 */ /** 前 4 支 API:平行取得組別與三個時段設定 */
async function loadPreferences() { async function loadPreferences() {
if (USE_MOCK) { if (USE_MOCK) {
await delay(MOCK_DELAY); await loadGroup();
group.value = "medium";
morning.value = "Y"; morning.value = "Y";
noon.value = "Y"; noon.value = "Y";
night.value = "Y"; night.value = "Y";
@@ -254,7 +293,7 @@ export const useCourseStore = defineStore("course", () => {
// 真實後端:course/group 由登入 Cookie 識別使用者; // 真實後端:course/group 由登入 Cookie 識別使用者;
// course/morning、noon、night 需要 group,兩者有相依順序。 // course/morning、noon、night 需要 group,兩者有相依順序。
const groupValue = await fetchGroup(); const groupValue = await loadGroup();
const [morningValue, noonValue, nightValue] = await Promise.all([ const [morningValue, noonValue, nightValue] = await Promise.all([
fetchMorning(groupValue), fetchMorning(groupValue),
@@ -338,10 +377,15 @@ export const useCourseStore = defineStore("course", () => {
courseList, courseList,
loading, loading,
error, error,
groupLoaded,
groupLoading,
groupError,
groupMeta, groupMeta,
selectedSlots, selectedSlots,
hasCourse, hasCourse,
loadAll, loadAll,
loadGroup,
resetGroup,
loadCourseList, loadCourseList,
loadPreferences, loadPreferences,
setSlot, setSlot,