fix: 文字
This commit is contained in:
@@ -11,20 +11,12 @@
|
||||
<v-card-title>
|
||||
{{ store.userProfile.phone }}
|
||||
</v-card-title>
|
||||
<v-card-subtitle>
|
||||
資料更新於 {{ todayTimeString }}
|
||||
</v-card-subtitle>
|
||||
<v-card-subtitle> 資料更新於 {{ todayTimeString }} </v-card-subtitle>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="courseStore.groupMissing"
|
||||
class="d-flex justify-center"
|
||||
cols="4"
|
||||
>
|
||||
<v-col v-if="courseStore.groupMissing" class="d-flex justify-center" cols="4">
|
||||
<!-- 體適能分組徽章 -->
|
||||
<div
|
||||
class="score-disk d-flex flex-column justify-center align-center text-primary"
|
||||
>
|
||||
<div class="score-disk d-flex flex-column justify-center align-center text-primary">
|
||||
<span class="group-label">未完成體適能測試</span>
|
||||
</div>
|
||||
</v-col>
|
||||
@@ -32,22 +24,12 @@
|
||||
|
||||
<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 class="ml-2" color="error" size="small" variant="text" @click="loadFitnessGroup">
|
||||
重試
|
||||
</v-btn>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-row
|
||||
v-if="RADAR_ENABLED && physicalFitnessRadarData"
|
||||
class="mt-4"
|
||||
no-gutters
|
||||
>
|
||||
<v-row v-if="RADAR_ENABLED && physicalFitnessRadarData" class="mt-4" no-gutters>
|
||||
<v-col
|
||||
v-for="summary in radarSummaries"
|
||||
:key="summary.label"
|
||||
@@ -195,23 +177,16 @@
|
||||
to="/register"
|
||||
variant="text"
|
||||
>
|
||||
建立帳號
|
||||
既有帳號建立密碼
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<div class="d-flex align-center mt-4 px-4">
|
||||
<v-icon
|
||||
class="mr-3"
|
||||
color="primary"
|
||||
icon="mdi-lock-outline"
|
||||
size="28"
|
||||
/>
|
||||
<v-icon class="mr-3" color="primary" icon="mdi-lock-outline" size="28" />
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-grey-darken-4">
|
||||
登入後可使用完整功能
|
||||
</div>
|
||||
<div class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
@@ -247,12 +222,7 @@
|
||||
>
|
||||
{{ generateResult }}
|
||||
</v-alert>
|
||||
<v-alert
|
||||
v-if="generateError"
|
||||
class="mb-0"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-alert v-if="generateError" class="mb-0" type="error" variant="tonal">
|
||||
{{ generateError }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
@@ -265,7 +235,7 @@
|
||||
variant="text"
|
||||
@click="closeGenerateDialog"
|
||||
>
|
||||
{{ generateResult || generateError ? "關閉" : "取消" }}
|
||||
{{ generateResult || generateError ? '關閉' : '取消' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="!generateResult && !generateError"
|
||||
@@ -289,11 +259,7 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-if="APPOINTMENT_ENABLED"
|
||||
v-model="missingGroupDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-dialog v-if="APPOINTMENT_ENABLED" v-model="missingGroupDialog" max-width="400">
|
||||
<v-card class="rounded-xl overflow-hidden text-center">
|
||||
<v-card-title class="bg-primary text-white font-weight-bold py-3">
|
||||
尚未取得體適能分組
|
||||
@@ -339,95 +305,92 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
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 FitnessGroupCardItem from "@/components/FitnessGroupCardItem.vue";
|
||||
import HealthRadarChart from "@/components/HealthRadarChart.vue";
|
||||
import IntroductionCarousel from "@/components/IntroductionCarousel.vue";
|
||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
||||
import ModuleRouterCard from "@/components/ModuleRouterCard.vue";
|
||||
import PlatformIntroduction from "@/components/PlatformIntroduction.vue";
|
||||
import PhysicalFitnessSummary from "@/components/PhysicalFitnessSummary.vue";
|
||||
import { computed, ref, watch } from 'vue'
|
||||
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 FitnessGroupCardItem from '@/components/FitnessGroupCardItem.vue'
|
||||
import HealthRadarChart from '@/components/HealthRadarChart.vue'
|
||||
import IntroductionCarousel from '@/components/IntroductionCarousel.vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import ModuleRouterCard from '@/components/ModuleRouterCard.vue'
|
||||
import PlatformIntroduction from '@/components/PlatformIntroduction.vue'
|
||||
import PhysicalFitnessSummary from '@/components/PhysicalFitnessSummary.vue'
|
||||
import {
|
||||
APPOINTMENT_ENABLED,
|
||||
BODY_COMPOSITION_ENABLED,
|
||||
CHECKIN_ENABLED,
|
||||
RADAR_ENABLED,
|
||||
} from "@/config/features";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCourseStore } from "@/stores/course";
|
||||
import { getApiErrorMessage } from "@/utils/api-error";
|
||||
import { calculatePhysicalFitnessRadar } from "@/utils/physical-fitness-radar";
|
||||
} from '@/config/features'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useCourseStore } from '@/stores/course'
|
||||
import { getApiErrorMessage } from '@/utils/api-error'
|
||||
import { calculatePhysicalFitnessRadar } from '@/utils/physical-fitness-radar'
|
||||
|
||||
const store = useAppStore();
|
||||
const courseStore = useCourseStore();
|
||||
const router = useRouter();
|
||||
const missingGroupDialog = ref(false);
|
||||
const generateDialog = ref(false);
|
||||
const generatePhysicalFitnessLoading = ref(false);
|
||||
const generateResult = ref("");
|
||||
const generateError = ref("");
|
||||
const physicalFitnessItems = ref<PhysicalFitnessItem[]>([]);
|
||||
const physicalFitnessReceivedAt = ref<string | null>(null);
|
||||
const physicalFitnessLoading = ref(false);
|
||||
const physicalFitnessError = ref("");
|
||||
const store = useAppStore()
|
||||
const courseStore = useCourseStore()
|
||||
const router = useRouter()
|
||||
const missingGroupDialog = ref(false)
|
||||
const generateDialog = ref(false)
|
||||
const generatePhysicalFitnessLoading = ref(false)
|
||||
const generateResult = ref('')
|
||||
const generateError = ref('')
|
||||
const physicalFitnessItems = ref<PhysicalFitnessItem[]>([])
|
||||
const physicalFitnessReceivedAt = ref<string | null>(null)
|
||||
const physicalFitnessLoading = ref(false)
|
||||
const physicalFitnessError = ref('')
|
||||
|
||||
const physicalFitnessRadarData = computed(() =>
|
||||
calculatePhysicalFitnessRadar(physicalFitnessItems.value),
|
||||
);
|
||||
calculatePhysicalFitnessRadar(physicalFitnessItems.value)
|
||||
)
|
||||
const radarSummaries = computed(() => {
|
||||
if (!physicalFitnessRadarData.value) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: "上肢",
|
||||
tip: "上肢肌力",
|
||||
label: '上肢',
|
||||
tip: '上肢肌力',
|
||||
score: physicalFitnessRadarData.value.upperBodyStrength,
|
||||
},
|
||||
{
|
||||
label: "下肢",
|
||||
tip: "下肢肌耐力",
|
||||
label: '下肢',
|
||||
tip: '下肢肌耐力',
|
||||
score: physicalFitnessRadarData.value.lowerBodyStrengthAndEndurance,
|
||||
},
|
||||
{
|
||||
label: "心肺",
|
||||
tip: "心肺耐力",
|
||||
label: '心肺',
|
||||
tip: '心肺耐力',
|
||||
score: physicalFitnessRadarData.value.cardio,
|
||||
},
|
||||
{
|
||||
label: "敏捷",
|
||||
tip: "平衡、敏捷與行動",
|
||||
label: '敏捷',
|
||||
tip: '平衡、敏捷與行動',
|
||||
score: physicalFitnessRadarData.value.balanceAgilityAndMobility,
|
||||
},
|
||||
{
|
||||
label: "柔軟",
|
||||
tip: "柔軟度",
|
||||
label: '柔軟',
|
||||
tip: '柔軟度',
|
||||
score: physicalFitnessRadarData.value.flexibility,
|
||||
},
|
||||
];
|
||||
});
|
||||
]
|
||||
})
|
||||
|
||||
const MISSING_GROUP_DIALOG_KEY = "missing-fitness-group-dialog-shown";
|
||||
const MISSING_GROUP_DIALOG_KEY = 'missing-fitness-group-dialog-shown'
|
||||
|
||||
function hasShownMissingGroupDialog() {
|
||||
try {
|
||||
return sessionStorage.getItem(MISSING_GROUP_DIALOG_KEY) === "true";
|
||||
return sessionStorage.getItem(MISSING_GROUP_DIALOG_KEY) === 'true'
|
||||
} catch {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function markMissingGroupDialogShown() {
|
||||
try {
|
||||
sessionStorage.setItem(MISSING_GROUP_DIALOG_KEY, "true");
|
||||
sessionStorage.setItem(MISSING_GROUP_DIALOG_KEY, 'true')
|
||||
} catch {
|
||||
// 部分瀏覽器可能封鎖 sessionStorage,仍允許本次彈窗正常使用。
|
||||
}
|
||||
@@ -435,154 +398,144 @@ function markMissingGroupDialogShown() {
|
||||
|
||||
function clearMissingGroupDialogMarker() {
|
||||
try {
|
||||
sessionStorage.removeItem(MISSING_GROUP_DIALOG_KEY);
|
||||
sessionStorage.removeItem(MISSING_GROUP_DIALOG_KEY)
|
||||
} catch {
|
||||
// 不影響主要流程。
|
||||
}
|
||||
}
|
||||
|
||||
function openMissingGroupDialog() {
|
||||
if (
|
||||
store.isLoggedIn &&
|
||||
courseStore.groupMissing &&
|
||||
!hasShownMissingGroupDialog()
|
||||
) {
|
||||
markMissingGroupDialogShown();
|
||||
missingGroupDialog.value = true;
|
||||
if (store.isLoggedIn && courseStore.groupMissing && !hasShownMissingGroupDialog()) {
|
||||
markMissingGroupDialogShown()
|
||||
missingGroupDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function goToAppointment() {
|
||||
missingGroupDialog.value = false;
|
||||
await router.push({ name: "appointment" });
|
||||
missingGroupDialog.value = false
|
||||
await router.push({ name: 'appointment' })
|
||||
}
|
||||
|
||||
async function loadFitnessGroup() {
|
||||
if (!store.isLoggedIn) {
|
||||
missingGroupDialog.value = false;
|
||||
clearMissingGroupDialogMarker();
|
||||
courseStore.resetGroup();
|
||||
return;
|
||||
missingGroupDialog.value = false
|
||||
clearMissingGroupDialogMarker()
|
||||
courseStore.resetGroup()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await courseStore.loadGroup();
|
||||
clearMissingGroupDialogMarker();
|
||||
await courseStore.loadGroup()
|
||||
clearMissingGroupDialogMarker()
|
||||
} catch {
|
||||
// 錯誤狀態由 course store 提供給畫面顯示與重試。
|
||||
openMissingGroupDialog();
|
||||
openMissingGroupDialog()
|
||||
}
|
||||
}
|
||||
|
||||
function getRequiredUserID(): string | null {
|
||||
const userID = store.userID.trim();
|
||||
const userID = store.userID.trim()
|
||||
|
||||
if (userID) {
|
||||
return userID;
|
||||
return userID
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
async function loadPhysicalFitness() {
|
||||
if (!store.isLoggedIn) {
|
||||
physicalFitnessItems.value = [];
|
||||
physicalFitnessReceivedAt.value = null;
|
||||
physicalFitnessError.value = "";
|
||||
return;
|
||||
physicalFitnessItems.value = []
|
||||
physicalFitnessReceivedAt.value = null
|
||||
physicalFitnessError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const userID = getRequiredUserID();
|
||||
const userID = getRequiredUserID()
|
||||
if (!userID) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
physicalFitnessLoading.value = true;
|
||||
physicalFitnessError.value = "";
|
||||
physicalFitnessLoading.value = true
|
||||
physicalFitnessError.value = ''
|
||||
|
||||
try {
|
||||
const physicalFitnessResponse = await fetchPhysicalFitness(userID);
|
||||
physicalFitnessItems.value = physicalFitnessResponse.results;
|
||||
physicalFitnessReceivedAt.value = physicalFitnessResponse.ReceivedAt;
|
||||
updateTodayTimeString();
|
||||
const physicalFitnessResponse = await fetchPhysicalFitness(userID)
|
||||
physicalFitnessItems.value = physicalFitnessResponse.results
|
||||
physicalFitnessReceivedAt.value = physicalFitnessResponse.ReceivedAt
|
||||
updateTodayTimeString()
|
||||
} catch (error) {
|
||||
physicalFitnessError.value = getApiErrorMessage(
|
||||
error,
|
||||
"體適能測驗資料載入失敗,請稍後再試。",
|
||||
);
|
||||
physicalFitnessError.value = getApiErrorMessage(error, '體適能測驗資料載入失敗,請稍後再試。')
|
||||
} finally {
|
||||
physicalFitnessLoading.value = false;
|
||||
physicalFitnessLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openGenerateDialog() {
|
||||
if (!getRequiredUserID()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
generateResult.value = "";
|
||||
generateError.value = "";
|
||||
generateDialog.value = true;
|
||||
generateResult.value = ''
|
||||
generateError.value = ''
|
||||
generateDialog.value = true
|
||||
}
|
||||
|
||||
function closeGenerateDialog() {
|
||||
if (generatePhysicalFitnessLoading.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
generateDialog.value = false;
|
||||
generateDialog.value = false
|
||||
}
|
||||
|
||||
async function handleGeneratePhysicalFitness() {
|
||||
const userID = getRequiredUserID();
|
||||
const userID = getRequiredUserID()
|
||||
if (!userID) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
generatePhysicalFitnessLoading.value = true;
|
||||
generateResult.value = "";
|
||||
generateError.value = "";
|
||||
generatePhysicalFitnessLoading.value = true
|
||||
generateResult.value = ''
|
||||
generateError.value = ''
|
||||
|
||||
try {
|
||||
const response = await generatePhysicalFitness(userID);
|
||||
generateResult.value = response.message;
|
||||
updateTodayTimeString();
|
||||
const response = await generatePhysicalFitness(userID)
|
||||
generateResult.value = response.message
|
||||
updateTodayTimeString()
|
||||
|
||||
if (response.message === "評級完成") {
|
||||
await courseStore.loadGroup(true);
|
||||
if (response.message === '評級完成') {
|
||||
await courseStore.loadGroup(true)
|
||||
}
|
||||
|
||||
await loadPhysicalFitness();
|
||||
await loadPhysicalFitness()
|
||||
} catch (error) {
|
||||
generateError.value = getApiErrorMessage(
|
||||
error,
|
||||
"體適能評級重新計算失敗,請稍後再試。",
|
||||
);
|
||||
generateError.value = getApiErrorMessage(error, '體適能評級重新計算失敗,請稍後再試。')
|
||||
} finally {
|
||||
generatePhysicalFitnessLoading.value = false;
|
||||
generatePhysicalFitnessLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true });
|
||||
watch(() => store.isLoggedIn, loadFitnessGroup, { immediate: true })
|
||||
watch([() => store.isLoggedIn, () => store.userID], loadPhysicalFitness, {
|
||||
immediate: true,
|
||||
});
|
||||
})
|
||||
|
||||
function formatCurrentDateTime() {
|
||||
const now = new Date();
|
||||
const now = new Date()
|
||||
const date = [
|
||||
String(now.getMonth() + 1).padStart(2, "0"),
|
||||
String(now.getDate()).padStart(2, "0"),
|
||||
].join("/");
|
||||
const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
String(now.getMonth() + 1).padStart(2, '0'),
|
||||
String(now.getDate()).padStart(2, '0'),
|
||||
].join('/')
|
||||
const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
|
||||
return `${date} ${time}`;
|
||||
return `${date} ${time}`
|
||||
}
|
||||
|
||||
const todayTimeString = ref(formatCurrentDateTime());
|
||||
const todayTimeString = ref(formatCurrentDateTime())
|
||||
|
||||
function updateTodayTimeString() {
|
||||
todayTimeString.value = formatCurrentDateTime();
|
||||
todayTimeString.value = formatCurrentDateTime()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="text-center">
|
||||
<BrandAvatar />
|
||||
<v-card-title class="text-primary font-weight-bold">
|
||||
建立帳號
|
||||
建立密碼
|
||||
</v-card-title>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
Reference in New Issue
Block a user