Compare commits
3 Commits
40a166d420
...
2ccb073f16
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ccb073f16 | |||
| 87aa5c9e33 | |||
| 1cfd948fec |
@@ -6,6 +6,7 @@ import type {
|
|||||||
YesNo,
|
YesNo,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { api } from "./client";
|
import { api } from "./client";
|
||||||
|
import { getApiErrorBody, getApiErrorStatus } from "@/utils/api-error";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 課程建議的 5 支 API。
|
* 課程建議的 5 支 API。
|
||||||
@@ -41,8 +42,20 @@ function toCourseRow(raw: CourseListApiRow): CourseRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 依登入 Cookie 取得目前使用者的體適能組別。 */
|
/** 依登入 Cookie 取得目前使用者的體適能組別。 */
|
||||||
export function fetchGroup(): Promise<FitnessGroup | ""> {
|
export async function fetchGroup(): Promise<FitnessGroup | ""> {
|
||||||
return api.get("course/group").json<FitnessGroup | "">();
|
try {
|
||||||
|
return await api.get("course/group").json<FitnessGroup | "">();
|
||||||
|
} catch (error) {
|
||||||
|
// 尚未完成分組時,後端以 404 與業務訊息回應,轉成 store 可處理的空組別。
|
||||||
|
if (
|
||||||
|
getApiErrorStatus(error) === 404 &&
|
||||||
|
getApiErrorBody(error)?.message === "尚未建立體適能組別"
|
||||||
|
) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取得是否納入早上時段 */
|
/** 取得是否納入早上時段 */
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type {
|
||||||
|
GeneratePhysicalFitnessResponse,
|
||||||
|
PhysicalFitnessItem,
|
||||||
|
} from "./types";
|
||||||
|
import { api } from "./client";
|
||||||
|
|
||||||
|
/** 取得指定使用者的 11 項最新體適能測驗成績。 */
|
||||||
|
export function fetchPhysicalFitness(
|
||||||
|
userID: string,
|
||||||
|
): Promise<PhysicalFitnessItem[]> {
|
||||||
|
return api
|
||||||
|
.get(`v1/physical-fitness/${encodeURIComponent(userID)}`)
|
||||||
|
.json<PhysicalFitnessItem[]>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 產生指定使用者的體適能評級。 */
|
||||||
|
export function generatePhysicalFitness(
|
||||||
|
userID: string,
|
||||||
|
): Promise<GeneratePhysicalFitnessResponse> {
|
||||||
|
return api
|
||||||
|
.post(`v1/physical-fitness/generate/${encodeURIComponent(userID)}`)
|
||||||
|
.json<GeneratePhysicalFitnessResponse>();
|
||||||
|
}
|
||||||
@@ -5,6 +5,18 @@ export type ConsentVersion = typeof CONSENT_VERSION;
|
|||||||
/** 體適能組別 */
|
/** 體適能組別 */
|
||||||
export type FitnessGroup = "low" | "medium" | "high";
|
export type FitnessGroup = "low" | "medium" | "high";
|
||||||
|
|
||||||
|
/** PhysicalFitnessController 回傳的單項測驗成績。 */
|
||||||
|
export interface PhysicalFitnessItem {
|
||||||
|
fitnessCode: string;
|
||||||
|
fitnessName: string;
|
||||||
|
fitnessScore: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PhysicalFitnessController 產生評級的回應。 */
|
||||||
|
export interface GeneratePhysicalFitnessResponse {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 性別 */
|
/** 性別 */
|
||||||
export type Gender = "male" | "female";
|
export type Gender = "male" | "female";
|
||||||
|
|
||||||
@@ -25,7 +37,6 @@ export type PainLevel = "none" | "mild" | "moderate" | "severe";
|
|||||||
|
|
||||||
/** 慢性病項目 */
|
/** 慢性病項目 */
|
||||||
export type ChronicDisease =
|
export type ChronicDisease =
|
||||||
| "none"
|
|
||||||
| "hypertension"
|
| "hypertension"
|
||||||
| "diabetes"
|
| "diabetes"
|
||||||
| "hyperlipidemia"
|
| "hyperlipidemia"
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
<v-card
|
<v-card
|
||||||
class="rounded-xl elevation-2 d-flex align-center justify-between pa-4 my-4"
|
class="rounded-xl elevation-2 d-flex align-center justify-between pa-4 my-4"
|
||||||
color="surface"
|
color="surface"
|
||||||
|
:disabled="disabled"
|
||||||
|
:link="Boolean(to || clickable)"
|
||||||
|
:loading="loading"
|
||||||
:to="to"
|
:to="to"
|
||||||
|
@click="emit('click', $event)"
|
||||||
>
|
>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<v-avatar
|
<v-avatar
|
||||||
@@ -22,16 +26,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
<v-icon v-if="to" color="primary" icon="mdi-chevron-right" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
to: string;
|
to?: string;
|
||||||
|
clickable?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
icon: string;
|
icon: string;
|
||||||
iconColor: string;
|
iconColor: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
clickable: false,
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: [event: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<v-card class="rounded-xl elevation-2 mt-4" color="surface">
|
||||||
|
<v-card-title class="d-flex align-center justify-space-between">
|
||||||
|
<span>11 項體適能測驗</span>
|
||||||
|
<div class="d-flex align-center ga-2">
|
||||||
|
<v-btn
|
||||||
|
aria-label="重新取得體適能資料"
|
||||||
|
color="primary"
|
||||||
|
:disabled="loading"
|
||||||
|
icon="mdi-refresh"
|
||||||
|
:loading="loading"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
/>
|
||||||
|
<v-chip color="primary" size="small" variant="tonal">
|
||||||
|
{{ completedCount }}/{{ displayItems.length }} 項有資料
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-subtitle> 顯示目前帳號各測驗項目的最新成績。 </v-card-subtitle>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="loading"
|
||||||
|
class="mb-4"
|
||||||
|
color="primary"
|
||||||
|
indeterminate
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="error"
|
||||||
|
class="mb-4"
|
||||||
|
density="compact"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
<v-btn
|
||||||
|
class="ml-2"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
>
|
||||||
|
重試
|
||||||
|
</v-btn>
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-row density="comfortable">
|
||||||
|
<v-col
|
||||||
|
v-for="item in displayItems"
|
||||||
|
:key="item.fitnessCode"
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="test-item d-flex align-center justify-space-between rounded-lg pa-3"
|
||||||
|
>
|
||||||
|
<div class="mr-3">
|
||||||
|
<div class="text-body-2 font-weight-bold">
|
||||||
|
{{ item.fitnessName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-body-1 font-weight-black text-no-wrap"
|
||||||
|
:class="item.fitnessScore === null ? 'text-grey' : 'text-primary'"
|
||||||
|
>
|
||||||
|
{{ item.fitnessScore ?? "無資料" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { PhysicalFitnessItem } from "@/api/types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
error: string;
|
||||||
|
items: PhysicalFitnessItem[];
|
||||||
|
loading: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
refresh: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const FITNESS_TEST_DEFINITIONS = [
|
||||||
|
{ code: "UPGO_244M", name: "八英呎起走(秒)", aliases: [] },
|
||||||
|
{ code: "SIT_STAND_30S", name: "30 秒坐站(次)", aliases: ["SIT30S"] },
|
||||||
|
{ code: "SIT_REACH_L", name: "坐姿體前彎左側(公分)", aliases: [] },
|
||||||
|
{ code: "SIT_REACH_R", name: "坐姿體前彎右側(公分)", aliases: [] },
|
||||||
|
{ code: "ARM_CURL_L", name: "30 秒手臂彎舉左側(次)", aliases: [] },
|
||||||
|
{ code: "ARM_CURL_R", name: "30 秒手臂彎舉右側(次)", aliases: [] },
|
||||||
|
{ code: "BACK_SCRATCH_L", name: "抓背測驗左側(公分)", aliases: [] },
|
||||||
|
{ code: "BACK_SCRATCH_R", name: "抓背測驗右側(公分)", aliases: [] },
|
||||||
|
{ code: "ONE_LEG_STAND_L", name: "單腳站立左側(秒)", aliases: [] },
|
||||||
|
{ code: "ONE_LEG_STAND_R", name: "單腳站立右側(秒)", aliases: [] },
|
||||||
|
{ code: "STEP_TEST_30S", name: "30 秒抬膝踏步(次)", aliases: [] },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const displayItems = computed(() =>
|
||||||
|
FITNESS_TEST_DEFINITIONS.map((definition) => {
|
||||||
|
const item = props.items.find(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.fitnessCode === definition.code ||
|
||||||
|
definition.aliases.some((alias) => alias === candidate.fitnessCode),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fitnessCode: definition.code,
|
||||||
|
fitnessName: item?.fitnessName || definition.name,
|
||||||
|
fitnessScore: item?.fitnessScore ?? null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const completedCount = computed(
|
||||||
|
() => displayItems.value.filter((item) => item.fitnessScore !== null).length,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-item {
|
||||||
|
min-height: 72px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
background: rgba(var(--v-theme-primary), 0.03);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -127,6 +127,24 @@
|
|||||||
<HealthRadarChart :radar-data="store.radarData" />
|
<HealthRadarChart :radar-data="store.radarData" />
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<PhysicalFitnessSummary
|
||||||
|
:error="physicalFitnessError"
|
||||||
|
:items="physicalFitnessItems"
|
||||||
|
:loading="physicalFitnessLoading"
|
||||||
|
@refresh="loadPhysicalFitness"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModuleRouterCard
|
||||||
|
clickable
|
||||||
|
description="依目前 11 項測驗成績重新計算體適能分組。"
|
||||||
|
icon="mdi-refresh-circle"
|
||||||
|
icon-color="secondary"
|
||||||
|
:disabled="generatePhysicalFitnessLoading"
|
||||||
|
:loading="generatePhysicalFitnessLoading"
|
||||||
|
title="重新計算體適能評級"
|
||||||
|
@click="openGenerateDialog"
|
||||||
|
/>
|
||||||
|
|
||||||
<ModuleRouterCard
|
<ModuleRouterCard
|
||||||
description="了解公益普測計畫、研究依據與平台流程。"
|
description="了解公益普測計畫、研究依據與平台流程。"
|
||||||
icon="mdi-information-outline"
|
icon="mdi-information-outline"
|
||||||
@@ -237,6 +255,75 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<v-dialog v-model="generateDialog" max-width="400">
|
||||||
|
<v-card class="rounded-xl overflow-hidden">
|
||||||
|
<v-card-title class="bg-primary text-white font-weight-bold py-3">
|
||||||
|
重新計算體適能評級
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<template v-if="!generateResult && !generateError">
|
||||||
|
<v-avatar class="mb-4" color="primary" size="64">
|
||||||
|
<v-icon color="white" icon="mdi-refresh-circle" size="34" />
|
||||||
|
</v-avatar>
|
||||||
|
<p class="text-body-1 font-weight-bold text-grey-darken-4 mb-2">
|
||||||
|
確定要重新計算目前的體適能評級嗎?
|
||||||
|
</p>
|
||||||
|
<p class="text-body-2 text-grey-darken-2 mb-0">
|
||||||
|
系統會依各測驗項目的最新成績重新計算分組。
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="generateResult"
|
||||||
|
class="mb-0"
|
||||||
|
:type="generateResult === '評級完成' ? 'success' : 'warning'"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ generateResult }}
|
||||||
|
</v-alert>
|
||||||
|
<v-alert
|
||||||
|
v-if="generateError"
|
||||||
|
class="mb-0"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ generateError }}
|
||||||
|
</v-alert>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4 justify-end">
|
||||||
|
<v-btn
|
||||||
|
:disabled="generatePhysicalFitnessLoading"
|
||||||
|
variant="text"
|
||||||
|
@click="closeGenerateDialog"
|
||||||
|
>
|
||||||
|
{{ generateResult || generateError ? "關閉" : "取消" }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="!generateResult && !generateError"
|
||||||
|
color="primary"
|
||||||
|
:loading="generatePhysicalFitnessLoading"
|
||||||
|
variant="flat"
|
||||||
|
@click="handleGeneratePhysicalFitness"
|
||||||
|
>
|
||||||
|
確認重新計算
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-else-if="generateError"
|
||||||
|
color="primary"
|
||||||
|
:loading="generatePhysicalFitnessLoading"
|
||||||
|
variant="flat"
|
||||||
|
@click="handleGeneratePhysicalFitness"
|
||||||
|
>
|
||||||
|
重試
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<v-dialog v-model="missingGroupDialog" max-width="400">
|
<v-dialog v-model="missingGroupDialog" max-width="400">
|
||||||
<v-card class="rounded-xl overflow-hidden text-center">
|
<v-card class="rounded-xl overflow-hidden text-center">
|
||||||
<v-card-title class="bg-primary text-white font-weight-bold py-3">
|
<v-card-title class="bg-primary text-white font-weight-bold py-3">
|
||||||
@@ -285,10 +372,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import type { PhysicalFitnessItem } from "@/api/types";
|
||||||
|
import {
|
||||||
|
fetchPhysicalFitness,
|
||||||
|
generatePhysicalFitness,
|
||||||
|
} from "@/api/physical-fitness";
|
||||||
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 PhysicalFitnessSummary from "@/components/PhysicalFitnessSummary.vue";
|
||||||
import { useDisplayUserName } from "@/composables/useDisplayUserName";
|
import { useDisplayUserName } from "@/composables/useDisplayUserName";
|
||||||
import {
|
import {
|
||||||
APPOINTMENT_ENABLED,
|
APPOINTMENT_ENABLED,
|
||||||
@@ -297,11 +390,20 @@ import {
|
|||||||
} from "@/config/features";
|
} from "@/config/features";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useCourseStore } from "@/stores/course";
|
import { useCourseStore } from "@/stores/course";
|
||||||
|
import { getApiErrorMessage } from "@/utils/api-error";
|
||||||
|
|
||||||
const store = useAppStore();
|
const store = useAppStore();
|
||||||
const courseStore = useCourseStore();
|
const courseStore = useCourseStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const missingGroupDialog = ref(false);
|
const missingGroupDialog = ref(false);
|
||||||
|
const generateDialog = ref(false);
|
||||||
|
const generatePhysicalFitnessLoading = ref(false);
|
||||||
|
const generateResult = ref("");
|
||||||
|
const generateError = ref("");
|
||||||
|
const physicalFitnessItems = ref<PhysicalFitnessItem[]>([]);
|
||||||
|
const physicalFitnessLoading = ref(false);
|
||||||
|
const physicalFitnessError = ref("");
|
||||||
|
const redirectingToLogin = ref(false);
|
||||||
|
|
||||||
const displayUserName = useDisplayUserName(store.userProfile);
|
const displayUserName = useDisplayUserName(store.userProfile);
|
||||||
|
|
||||||
@@ -364,7 +466,99 @@ async function loadFitnessGroup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRequiredUserID(): string | null {
|
||||||
|
const userID = store.userID.trim();
|
||||||
|
|
||||||
|
if (userID) {
|
||||||
|
return userID;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!redirectingToLogin.value) {
|
||||||
|
redirectingToLogin.value = true;
|
||||||
|
void router.replace({ name: "login", query: { redirect: "/" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPhysicalFitness() {
|
||||||
|
if (!store.isLoggedIn) {
|
||||||
|
physicalFitnessItems.value = [];
|
||||||
|
physicalFitnessError.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userID = getRequiredUserID();
|
||||||
|
if (!userID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalFitnessLoading.value = true;
|
||||||
|
physicalFitnessError.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
physicalFitnessItems.value = await fetchPhysicalFitness(userID);
|
||||||
|
} catch (error) {
|
||||||
|
physicalFitnessError.value = getApiErrorMessage(
|
||||||
|
error,
|
||||||
|
"體適能測驗資料載入失敗,請稍後再試。",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
physicalFitnessLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGenerateDialog() {
|
||||||
|
if (!getRequiredUserID()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateResult.value = "";
|
||||||
|
generateError.value = "";
|
||||||
|
generateDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGenerateDialog() {
|
||||||
|
if (generatePhysicalFitnessLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDialog.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGeneratePhysicalFitness() {
|
||||||
|
const userID = getRequiredUserID();
|
||||||
|
if (!userID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePhysicalFitnessLoading.value = true;
|
||||||
|
generateResult.value = "";
|
||||||
|
generateError.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await generatePhysicalFitness(userID);
|
||||||
|
generateResult.value = response.message;
|
||||||
|
|
||||||
|
if (response.message === "評級完成") {
|
||||||
|
await courseStore.loadGroup(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadPhysicalFitness();
|
||||||
|
} catch (error) {
|
||||||
|
generateError.value = getApiErrorMessage(
|
||||||
|
error,
|
||||||
|
"體適能評級重新計算失敗,請稍後再試。",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
generatePhysicalFitnessLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true });
|
watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true });
|
||||||
|
watch([() => store.isLoggedIn, () => store.userID], loadPhysicalFitness, {
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
const todayTimeString = computed(() => {
|
const todayTimeString = computed(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@@ -372,9 +372,6 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
@update:model-value="
|
|
||||||
handleChronicDiseaseChange(disease.value, $event)
|
|
||||||
"
|
|
||||||
:label="disease.title"
|
:label="disease.title"
|
||||||
:value="disease.value"
|
:value="disease.value"
|
||||||
/>
|
/>
|
||||||
@@ -617,7 +614,6 @@ const improvementsOptions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const diseaseOptions = [
|
const diseaseOptions = [
|
||||||
{ title: "無", value: "none" as const },
|
|
||||||
{ title: "高血壓", value: "hypertension" as const },
|
{ title: "高血壓", value: "hypertension" as const },
|
||||||
{ title: "糖尿病", value: "diabetes" as const },
|
{ title: "糖尿病", value: "diabetes" as const },
|
||||||
{ title: "高血脂", value: "hyperlipidemia" as const },
|
{ title: "高血脂", value: "hyperlipidemia" as const },
|
||||||
@@ -626,21 +622,6 @@ const diseaseOptions = [
|
|||||||
{ title: "骨質疏鬆", value: "osteoporosis" as const },
|
{ title: "骨質疏鬆", value: "osteoporosis" as const },
|
||||||
];
|
];
|
||||||
|
|
||||||
function handleChronicDiseaseChange(
|
|
||||||
changedDisease: ChronicDisease,
|
|
||||||
selectedDiseases: ChronicDisease[] | null,
|
|
||||||
) {
|
|
||||||
const normalizedDiseases = selectedDiseases ?? [];
|
|
||||||
|
|
||||||
if (changedDisease === "none" && normalizedDiseases.includes("none")) {
|
|
||||||
assessmentForm.chronicDiseases = ["none"];
|
|
||||||
} else if (changedDisease !== "none") {
|
|
||||||
assessmentForm.chronicDiseases = normalizedDiseases.filter(
|
|
||||||
(disease) => disease !== "none",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const birthdayDate = computed<Date | null>({
|
const birthdayDate = computed<Date | null>({
|
||||||
get: () => parseDateString(profileForm.birthday),
|
get: () => parseDateString(profileForm.birthday),
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ const WANTED_IMPROVEMENT_VALUES: WantedImprovement[] = [
|
|||||||
"bodyComposition",
|
"bodyComposition",
|
||||||
];
|
];
|
||||||
const CHRONIC_DISEASE_VALUES: ChronicDisease[] = [
|
const CHRONIC_DISEASE_VALUES: ChronicDisease[] = [
|
||||||
"none",
|
|
||||||
"hypertension",
|
"hypertension",
|
||||||
"diabetes",
|
"diabetes",
|
||||||
"hyperlipidemia",
|
"hyperlipidemia",
|
||||||
|
|||||||
Reference in New Issue
Block a user