style: index page
This commit is contained in:
@@ -9,3 +9,7 @@
|
||||
- Framework: Vue 3 + Vite
|
||||
- UI Library: Vuetify
|
||||
- Enabled Features: ESLint, Pinia, Vue I18n, Vue Router
|
||||
|
||||
## UI Construction
|
||||
- Prefer Vuetify built-in UI components and utility classes when building pages.
|
||||
- Use custom CSS only when Vuetify components or utilities cannot express the required layout, spacing, color, or interaction clearly.
|
||||
|
||||
@@ -9,5 +9,5 @@ export default vuetify(
|
||||
'vue/script-indent': 'off',
|
||||
'vue/html-self-closing': 'off',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.2.10",
|
||||
"@mdi/font": "7.4.47",
|
||||
"vue": "^3.5.30",
|
||||
"vuetify": "^4.0.2",
|
||||
@@ -29,16 +28,10 @@
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-fonts": "^1.4.0",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-vuetify": "^2.1.3",
|
||||
"vue-tsc": "^3.2.5",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-vuetify": "^4.3.4"
|
||||
},
|
||||
"overrides": {
|
||||
"unplugin-fonts": {
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="d-flex justify-center my-2 position-relative">
|
||||
<svg class="radar-svg" height="260" viewBox="0 0 120 120" width="260">
|
||||
<polygon
|
||||
v-for="grid in [20, 40, 60, 80, 100]"
|
||||
:key="grid"
|
||||
fill="none"
|
||||
:points="getPolygonPoints(grid)"
|
||||
stroke="#E0E0E0"
|
||||
stroke-width="0.3"
|
||||
/>
|
||||
|
||||
<line
|
||||
v-for="i in 5"
|
||||
:key="`axis-${i}`"
|
||||
stroke="#E0E0E0"
|
||||
stroke-width="0.3"
|
||||
x1="60"
|
||||
:x2="getAxisEndPoint(i - 1).x"
|
||||
y1="60"
|
||||
:y2="getAxisEndPoint(i - 1).y"
|
||||
/>
|
||||
|
||||
<polygon
|
||||
fill="rgba(33, 150, 243, 0.12)"
|
||||
:points="getPolygonPoints(65)"
|
||||
stroke="#2196F3"
|
||||
stroke-dasharray="1 1"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
|
||||
<polygon
|
||||
fill="rgba(71, 194, 166, 0.35)"
|
||||
:points="userDataPoints"
|
||||
stroke="#1D7063"
|
||||
stroke-width="1.2"
|
||||
/>
|
||||
|
||||
<circle
|
||||
v-for="(point, index) in userDataCoordArray"
|
||||
:key="`dot-${index}`"
|
||||
:cx="point.x"
|
||||
:cy="point.y"
|
||||
fill="#E66926"
|
||||
r="1.5"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
|
||||
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="middle" x="60" y="8">
|
||||
心肺 ({{ radarData.cardio }})
|
||||
</text>
|
||||
|
||||
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="start" x="110" y="44">
|
||||
肌力 ({{ radarData.strength }})
|
||||
</text>
|
||||
|
||||
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="start" x="94" y="104">
|
||||
平衡 ({{ radarData.balance }})
|
||||
</text>
|
||||
|
||||
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="end" x="26" y="104">
|
||||
柔軟 ({{ radarData.flexibility }})
|
||||
</text>
|
||||
|
||||
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="end" x="10" y="44">
|
||||
敏捷 ({{ radarData.agility }})
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
<!-- <div class="radar-legend d-flex flex-column">
|
||||
<div class="d-flex align-center text-caption mb-1">
|
||||
<span class="legend-color bg-primary mr-1" />
|
||||
<span>我的指標</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center text-caption">
|
||||
<span class="legend-color bg-blue mr-1 border-dashed" />
|
||||
<span>同齡參考</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface RadarData {
|
||||
strength: number
|
||||
balance: number
|
||||
flexibility: number
|
||||
agility: number
|
||||
cardio: number
|
||||
}
|
||||
|
||||
const { radarData } = defineProps<{
|
||||
radarData: RadarData
|
||||
}>()
|
||||
|
||||
const centerX = 60
|
||||
const centerY = 60
|
||||
const radius = 45
|
||||
|
||||
function getAxisEndPoint(index: number, pointRadius = radius) {
|
||||
const angle = -Math.PI / 2 + (index * 2 * Math.PI) / 5
|
||||
|
||||
return {
|
||||
x: centerX + pointRadius * Math.cos(angle),
|
||||
y: centerY + pointRadius * Math.sin(angle),
|
||||
}
|
||||
}
|
||||
|
||||
function getPolygonPoints(percent: number) {
|
||||
const pointRadius = (percent / 100) * radius
|
||||
const points = []
|
||||
|
||||
for (let index = 0; index < 5; index++) {
|
||||
const point = getAxisEndPoint(index, pointRadius)
|
||||
points.push(`${point.x},${point.y}`)
|
||||
}
|
||||
|
||||
return points.join(' ')
|
||||
}
|
||||
|
||||
const userDataCoordArray = computed(() => {
|
||||
const values = [
|
||||
radarData.cardio,
|
||||
radarData.strength,
|
||||
radarData.balance,
|
||||
radarData.flexibility,
|
||||
radarData.agility,
|
||||
]
|
||||
|
||||
return values.map((value, index) => {
|
||||
const pointRadius = (Math.max(5, Math.min(100, value)) / 100) * radius
|
||||
return getAxisEndPoint(index, pointRadius)
|
||||
})
|
||||
})
|
||||
|
||||
const userDataPoints = computed(() => {
|
||||
return userDataCoordArray.value.map((point) => `${point.x},${point.y}`).join(' ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.radar-legend {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 6px 8px;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.border-dashed {
|
||||
background-color: rgba(33, 150, 243, 0.12) !important;
|
||||
border: 1px dashed #2196f3;
|
||||
}
|
||||
|
||||
.radar-svg text {
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<v-card
|
||||
class="rounded-xl elevation-2 d-flex align-center justify-between pa-4 my-4"
|
||||
color="surface"
|
||||
:to="to"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 text-white elevation-1" :color="iconColor" size="48">
|
||||
<v-icon :icon="icon" />
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">{{ title }}</div>
|
||||
|
||||
<div class="text-caption text-grey">{{ description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
to: string
|
||||
icon: string
|
||||
iconColor: string
|
||||
title: string
|
||||
description: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -21,18 +21,6 @@
|
||||
>
|
||||
<v-icon :color="isLargeText ? 'accent' : 'white'" icon="mdi-format-size" />
|
||||
</v-btn>
|
||||
|
||||
<v-avatar
|
||||
v-if="store.isLoggedIn"
|
||||
class="elevation-1 border-white"
|
||||
color="accent"
|
||||
size="36"
|
||||
@click="drawer = !drawer"
|
||||
>
|
||||
<span class="text-white font-weight-bold text-caption">{{
|
||||
store.userProfile.name.substring(1)
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
</div>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
@@ -189,7 +177,7 @@
|
||||
>
|
||||
<v-btn to="/" value="home">
|
||||
<v-icon :color="getBottomNavIconColor('home')">mdi-view-dashboard</v-icon>
|
||||
<span class="bottom-nav-text">主控台</span>
|
||||
<span class="bottom-nav-text">首頁</span>
|
||||
</v-btn>
|
||||
|
||||
<v-btn to="/self-assessment" value="self-assessment">
|
||||
@@ -197,17 +185,17 @@
|
||||
mdi-clipboard-text-search
|
||||
</v-icon>
|
||||
|
||||
<span class="bottom-nav-text">線上自評</span>
|
||||
<span class="bottom-nav-text">自評</span>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="store.isLoggedIn" to="/appointment" value="appointment">
|
||||
<v-icon :color="getBottomNavIconColor('appointment')">mdi-calendar-check</v-icon>
|
||||
<span class="bottom-nav-text">活動預約</span>
|
||||
<span class="bottom-nav-text">預約</span>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="store.isLoggedIn" to="/checkin" value="checkin">
|
||||
<v-icon :color="getBottomNavIconColor('checkin')">mdi-qrcode-scan</v-icon>
|
||||
<span class="bottom-nav-text">現場量測</span>
|
||||
<span class="bottom-nav-text">量測</span>
|
||||
</v-btn>
|
||||
</v-bottom-navigation>
|
||||
|
||||
@@ -307,27 +295,27 @@ const showEmergency = ref(false)
|
||||
// 無障礙狀態 (可持久化儲存於 localStorage)
|
||||
const isLargeText = ref(localStorage.getItem('mode-large-text') === 'true')
|
||||
|
||||
watch(isLargeText, newVal => {
|
||||
watch(isLargeText, (newVal) => {
|
||||
localStorage.setItem('mode-large-text', String(newVal))
|
||||
})
|
||||
|
||||
// 同步底部導航狀態與當前路由
|
||||
watch(
|
||||
() => route.path,
|
||||
path => {
|
||||
(path) => {
|
||||
if (path === '/') activeTab.value = 'home'
|
||||
else if (path.startsWith('/self-assessment')) activeTab.value = 'self-assessment'
|
||||
else if (path.startsWith('/appointment')) activeTab.value = 'appointment'
|
||||
else if (path.startsWith('/checkin')) activeTab.value = 'checkin'
|
||||
},
|
||||
{ immediate: true },
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function getBottomNavIconColor (value: string) {
|
||||
function getBottomNavIconColor(value: string) {
|
||||
return activeTab.value === value ? 'secondary' : 'secondary-darken-1'
|
||||
}
|
||||
|
||||
function handleLogout () {
|
||||
function handleLogout() {
|
||||
store.logout()
|
||||
router.push('/login')
|
||||
drawer.value = false
|
||||
|
||||
@@ -15,7 +15,6 @@ import App from './App.vue'
|
||||
|
||||
// Styles
|
||||
import '@/styles/fonts.css'
|
||||
import 'unfonts.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="text-subtitle-1 text-grey font-weight-bold">目前沒有相關活動</div>
|
||||
</div>
|
||||
|
||||
<v-row v-else dense>
|
||||
<v-row v-else>
|
||||
<v-col v-for="apt in filteredAppointments" :key="apt.id" class="mb-4" cols="12">
|
||||
<v-card class="rounded-xl elevation-3 border-accent-light" color="surface">
|
||||
<!-- 卡片頂部狀態條 -->
|
||||
@@ -63,7 +63,12 @@
|
||||
<v-icon class="mr-2 mt-0.5" color="primary" icon="mdi-human-capacity" size="18" />
|
||||
|
||||
<div class="text-body-2 text-grey-darken-3">
|
||||
剩餘名額:<span class="font-weight-bold" :class="apt.slots < 10 ? 'text-error' : 'text-success'">{{ apt.slots }}</span> 位
|
||||
剩餘名額:<span
|
||||
class="font-weight-bold"
|
||||
:class="apt.slots < 10 ? 'text-error' : 'text-success'"
|
||||
>{{ apt.slots }}</span
|
||||
>
|
||||
位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +111,7 @@
|
||||
|
||||
<!-- 已報名狀態 -->
|
||||
<template v-else-if="apt.status === '已報名'">
|
||||
<v-row class="w-100" dense>
|
||||
<v-row class="w-100">
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
block
|
||||
@@ -166,7 +171,9 @@
|
||||
<p class="text-caption text-grey mb-4">請在現場合對處出示此條碼進行報到</p>
|
||||
|
||||
<!-- 模擬 QR Code SVG,更高級精緻 -->
|
||||
<div class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center">
|
||||
<div
|
||||
class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center"
|
||||
>
|
||||
<svg class="qrcode-svg" height="180" viewBox="0 0 100 100" width="180">
|
||||
<!-- 四個角定位框 -->
|
||||
<path d="M 0,0 H 25 V 8 H 8 V 25 H 0 Z" fill="#1D7063" />
|
||||
@@ -174,142 +181,40 @@
|
||||
<path d="M 0,75 V 100 H 25 V 92 H 8 V 75 Z" fill="#1D7063" />
|
||||
<path d="M 92,75 V 92 H 75 V 100 H 100 V 75 Z" fill="#1D7063" />
|
||||
<!-- 內嵌二維碼方塊 -->
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="12"
|
||||
y="12"
|
||||
/>
|
||||
<rect fill="#1D7063" height="20" width="20" x="12" y="12" />
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="16"
|
||||
y="16"
|
||||
/>
|
||||
<rect fill="white" height="12" width="12" x="16" y="16" />
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="6"
|
||||
width="6"
|
||||
x="19"
|
||||
y="19"
|
||||
/>
|
||||
<rect fill="#E66926" height="6" width="6" x="19" y="19" />
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="68"
|
||||
y="12"
|
||||
/>
|
||||
<rect fill="#1D7063" height="20" width="20" x="68" y="12" />
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="72"
|
||||
y="16"
|
||||
/>
|
||||
<rect fill="white" height="12" width="12" x="72" y="16" />
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="6"
|
||||
width="6"
|
||||
x="75"
|
||||
y="75"
|
||||
/>
|
||||
<rect fill="#E66926" height="6" width="6" x="75" y="75" />
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="12"
|
||||
y="68"
|
||||
/>
|
||||
<rect fill="#1D7063" height="20" width="20" x="12" y="68" />
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="16"
|
||||
y="72"
|
||||
/>
|
||||
<rect fill="white" height="12" width="12" x="16" y="72" />
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="6"
|
||||
width="6"
|
||||
x="19"
|
||||
y="75"
|
||||
/>
|
||||
<rect fill="#1D7063" height="6" width="6" x="19" y="75" />
|
||||
|
||||
<!-- 中間隨機小方塊 -->
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="8"
|
||||
width="8"
|
||||
x="40"
|
||||
y="20"
|
||||
/>
|
||||
<rect fill="#1D7063" height="8" width="8" x="40" y="20" />
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="14"
|
||||
width="6"
|
||||
x="52"
|
||||
y="12"
|
||||
/>
|
||||
<rect fill="#E66926" height="14" width="6" x="52" y="12" />
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="6"
|
||||
width="16"
|
||||
x="42"
|
||||
y="36"
|
||||
/>
|
||||
<rect fill="#1D7063" height="6" width="16" x="42" y="36" />
|
||||
|
||||
<rect
|
||||
fill="#47C2A6"
|
||||
height="10"
|
||||
width="28"
|
||||
x="36"
|
||||
y="48"
|
||||
/>
|
||||
<rect fill="#47C2A6" height="10" width="28" x="36" y="48" />
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="12"
|
||||
width="14"
|
||||
x="68"
|
||||
y="42"
|
||||
/>
|
||||
<rect fill="#1D7063" height="12" width="14" x="68" y="42" />
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="16"
|
||||
width="16"
|
||||
x="72"
|
||||
y="60"
|
||||
/>
|
||||
<rect fill="#E66926" height="16" width="16" x="72" y="60" />
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="12"
|
||||
width="18"
|
||||
x="44"
|
||||
y="68"
|
||||
/>
|
||||
<rect fill="#1D7063" height="12" width="18" x="44" y="68" />
|
||||
|
||||
<rect
|
||||
fill="#47C2A6"
|
||||
height="6"
|
||||
width="24"
|
||||
x="40"
|
||||
y="82"
|
||||
/>
|
||||
<rect fill="#47C2A6" height="6" width="24" x="40" y="82" />
|
||||
</svg>
|
||||
<!-- 紅色掃描雷射線 -->
|
||||
<div class="laser-line" />
|
||||
@@ -335,7 +240,12 @@
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4 justify-center">
|
||||
<v-btn class="rounded-lg px-6" color="primary" variant="flat" @click="qrcodeDialog = false">
|
||||
<v-btn
|
||||
class="rounded-lg px-6"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
@click="qrcodeDialog = false"
|
||||
>
|
||||
關閉條碼
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -346,55 +256,59 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { type Appointment, useAppStore } from '@/stores/app'
|
||||
import { computed, ref } from 'vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { type Appointment, useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
const filterTab = ref('all')
|
||||
const store = useAppStore()
|
||||
const filterTab = ref('all')
|
||||
|
||||
const qrcodeDialog = ref(false)
|
||||
const selectedApt = ref<Appointment | null>(null)
|
||||
const qrcodeDialog = ref(false)
|
||||
const selectedApt = ref<Appointment | null>(null)
|
||||
|
||||
const filteredAppointments = computed(() => {
|
||||
if (filterTab.value === 'registered') {
|
||||
return store.appointments.filter(a => a.status === '已報名' || a.status === '已報到')
|
||||
const filteredAppointments = computed(() => {
|
||||
if (filterTab.value === 'registered') {
|
||||
return store.appointments.filter((a) => a.status === '已報名' || a.status === '已報到')
|
||||
}
|
||||
return store.appointments
|
||||
})
|
||||
|
||||
function getStatusColor(status: string) {
|
||||
switch (status) {
|
||||
case '未報名': {
|
||||
return 'grey-darken-1'
|
||||
}
|
||||
return store.appointments
|
||||
})
|
||||
|
||||
function getStatusColor (status: string) {
|
||||
switch (status) {
|
||||
case '未報名': { return 'grey-darken-1'
|
||||
}
|
||||
case '已報名': { return 'secondary'
|
||||
}
|
||||
case '已報到': { return 'success'
|
||||
}
|
||||
default: { return 'grey'
|
||||
}
|
||||
case '已報名': {
|
||||
return 'secondary'
|
||||
}
|
||||
case '已報到': {
|
||||
return 'success'
|
||||
}
|
||||
default: {
|
||||
return 'grey'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRegister (id: string) {
|
||||
store.registerAppointment(id)
|
||||
}
|
||||
function handleRegister(id: string) {
|
||||
store.registerAppointment(id)
|
||||
}
|
||||
|
||||
function handleCancel (id: string) {
|
||||
store.cancelAppointment(id)
|
||||
}
|
||||
function handleCancel(id: string) {
|
||||
store.cancelAppointment(id)
|
||||
}
|
||||
|
||||
function showCheckinQRCode (apt: Appointment) {
|
||||
selectedApt.value = apt
|
||||
qrcodeDialog.value = true
|
||||
}
|
||||
function showCheckinQRCode(apt: Appointment) {
|
||||
selectedApt.value = apt
|
||||
qrcodeDialog.value = true
|
||||
}
|
||||
|
||||
function simulateOnSiteCheckin () {
|
||||
if (selectedApt.value) {
|
||||
store.checkinAppointment(selectedApt.value.id)
|
||||
qrcodeDialog.value = false
|
||||
}
|
||||
function simulateOnSiteCheckin() {
|
||||
if (selectedApt.value) {
|
||||
store.checkinAppointment(selectedApt.value.id)
|
||||
qrcodeDialog.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -429,8 +343,14 @@
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { top: 0%; }
|
||||
50% { top: 100%; }
|
||||
100% { top: 0%; }
|
||||
0% {
|
||||
top: 0%;
|
||||
}
|
||||
50% {
|
||||
top: 100%;
|
||||
}
|
||||
100% {
|
||||
top: 0%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
</div>
|
||||
|
||||
<!-- 報到活動資訊 -->
|
||||
<v-card v-if="activeAppointment" class="rounded-xl elevation-3 border-accent-light mb-6" color="surface">
|
||||
<v-card
|
||||
v-if="activeAppointment"
|
||||
class="rounded-xl elevation-3 border-accent-light mb-6"
|
||||
color="surface"
|
||||
>
|
||||
<v-card-text class="pa-4">
|
||||
<div class="d-flex align-center justify-between mb-3">
|
||||
<span class="text-subtitle-2 font-weight-bold text-primary">
|
||||
@@ -32,7 +36,9 @@
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-space-between align-center mb-1">
|
||||
<span class="text-caption font-weight-bold text-grey-darken-2">測量站點完成度</span>
|
||||
<span class="text-caption font-weight-bold text-primary">{{ completedCount }} / {{ totalCount }} ({{ progressPercent }}%)</span>
|
||||
<span class="text-caption font-weight-bold text-primary"
|
||||
>{{ completedCount }} / {{ totalCount }} ({{ progressPercent }}%)</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<v-progress-linear
|
||||
@@ -51,12 +57,19 @@
|
||||
<v-card v-else class="rounded-xl elevation-3 pa-6 text-center mb-6" color="surface">
|
||||
<v-icon class="mb-3" color="warning" icon="mdi-qrcode-scan" size="64" />
|
||||
<h3 class="text-subtitle-1 font-weight-bold mb-2">尚未在活動現場合對報到</h3>
|
||||
<p class="text-body-2 text-grey mb-4">請先前往「活動預約」頁面,出示您的報到條碼給現場志工進行核對。</p>
|
||||
<p class="text-body-2 text-grey mb-4">
|
||||
請先前往「活動預約」頁面,出示您的報到條碼給現場志工進行核對。
|
||||
</p>
|
||||
<v-btn class="rounded-lg" color="primary" to="/appointment">前往查看預約活動</v-btn>
|
||||
</v-card>
|
||||
|
||||
<!-- 快捷測試功能 (對 Demo 非常友善) -->
|
||||
<v-card v-if="activeAppointment && completedCount < totalCount" class="rounded-xl mb-6 border-accent-light bg-teal-lighten-5" color="primary" variant="outlined">
|
||||
<v-card
|
||||
v-if="activeAppointment && completedCount < totalCount"
|
||||
class="rounded-xl mb-6 border-accent-light bg-teal-lighten-5"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-text class="pa-4 d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-subtitle-2 font-weight-bold text-primary">
|
||||
@@ -64,10 +77,17 @@
|
||||
高齡友善測試輔助
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey-darken-1">一鍵填入所有剩餘關卡數值,便於檢視報告</div>
|
||||
<div class="text-caption text-grey-darken-1">
|
||||
一鍵填入所有剩餘關卡數值,便於檢視報告
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-btn class="rounded-lg font-weight-bold text-white" color="secondary" size="small" @click="simulateAllMeasurements">
|
||||
<v-btn
|
||||
class="rounded-lg font-weight-bold text-white"
|
||||
color="secondary"
|
||||
size="small"
|
||||
@click="simulateAllMeasurements"
|
||||
>
|
||||
一鍵模擬完成
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
@@ -77,7 +97,7 @@
|
||||
<div v-if="activeAppointment" class="mb-6">
|
||||
<h3 class="text-subtitle-2 font-weight-black text-primary mb-3">關卡列表</h3>
|
||||
|
||||
<v-row dense>
|
||||
<v-row>
|
||||
<v-col v-for="(station, key) in store.measurements" :key="key" class="mb-2" cols="12">
|
||||
<v-card
|
||||
class="rounded-xl elevation-2 cursor-pointer transition-card"
|
||||
@@ -91,22 +111,28 @@
|
||||
:color="station.completed ? 'success' : 'grey-lighten-2'"
|
||||
size="40"
|
||||
>
|
||||
<v-icon :icon="station.completed ? 'mdi-check' : getStationIcon(key)" size="20" />
|
||||
<v-icon
|
||||
:icon="station.completed ? 'mdi-check' : getStationIcon(key)"
|
||||
size="20"
|
||||
/>
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-body-1" :class="station.completed ? 'text-success' : 'text-grey-darken-4'">
|
||||
<div
|
||||
class="font-weight-bold text-body-1"
|
||||
:class="station.completed ? 'text-success' : 'text-grey-darken-4'"
|
||||
>
|
||||
{{ station.name }}
|
||||
</div>
|
||||
<!-- 已完成,顯示數值 -->
|
||||
<div v-if="station.completed" class="text-caption text-grey-darken-2">
|
||||
{{ getMeasurementSummary(key, station.values) }}
|
||||
<span class="ml-2 text-grey-lighten-1">| {{ station.time?.split(' ')[1] }}</span>
|
||||
<span class="ml-2 text-grey-lighten-1"
|
||||
>| {{ station.time?.split(' ')[1] }}</span
|
||||
>
|
||||
</div>
|
||||
<!-- 未完成,顯示待測 -->
|
||||
<div v-else class="text-caption text-grey-darken-1">
|
||||
等待現場檢測員登錄
|
||||
</div>
|
||||
<div v-else class="text-caption text-grey-darken-1">等待現場檢測員登錄</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -162,137 +188,149 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
const router = useRouter()
|
||||
const reportSuccessDialog = ref(false)
|
||||
const store = useAppStore()
|
||||
const router = useRouter()
|
||||
const reportSuccessDialog = ref(false)
|
||||
|
||||
// 尋找已報到的活動
|
||||
const activeAppointment = computed(() => {
|
||||
return store.appointments.find(a => a.status === '已報到')
|
||||
})
|
||||
// 尋找已報到的活動
|
||||
const activeAppointment = computed(() => {
|
||||
return store.appointments.find((a) => a.status === '已報到')
|
||||
})
|
||||
|
||||
const completedCount = computed(() => store.completedStationsCount)
|
||||
const totalCount = computed(() => store.totalStationsCount)
|
||||
const progressPercent = computed(() => store.progressPercent)
|
||||
const completedCount = computed(() => store.completedStationsCount)
|
||||
const totalCount = computed(() => store.totalStationsCount)
|
||||
const progressPercent = computed(() => store.progressPercent)
|
||||
|
||||
function getStationIcon (code: string) {
|
||||
switch (code) {
|
||||
case 'BloodPressure': { return 'mdi-heart-flash'
|
||||
}
|
||||
case 'BodyFat': { return 'mdi-scale-bathroom'
|
||||
}
|
||||
case 'GripStrength': { return 'mdi-hand-back-left'
|
||||
}
|
||||
case 'Sit5x': { return 'mdi-human-male-height'
|
||||
}
|
||||
case 'Sit30s': { return 'mdi-chair-school'
|
||||
}
|
||||
case 'StepTest': { return 'mdi-run-fast'
|
||||
}
|
||||
case 'ArmTest': { return 'mdi-arm-flex'
|
||||
}
|
||||
case 'BackTest': { return 'mdi-human-stretching'
|
||||
}
|
||||
case 'SitReachTest': { return 'mdi-human-stretching'
|
||||
}
|
||||
case 'UpGo8ft': { return 'mdi-navigation'
|
||||
}
|
||||
case 'OneLegTest': { return 'mdi-scale-balance'
|
||||
}
|
||||
default: { return 'mdi-checkbox-blank-circle-outline'
|
||||
}
|
||||
function getStationIcon(code: string) {
|
||||
switch (code) {
|
||||
case 'BloodPressure': {
|
||||
return 'mdi-heart-flash'
|
||||
}
|
||||
case 'BodyFat': {
|
||||
return 'mdi-scale-bathroom'
|
||||
}
|
||||
case 'GripStrength': {
|
||||
return 'mdi-hand-back-left'
|
||||
}
|
||||
case 'Sit5x': {
|
||||
return 'mdi-human-male-height'
|
||||
}
|
||||
case 'Sit30s': {
|
||||
return 'mdi-chair-school'
|
||||
}
|
||||
case 'StepTest': {
|
||||
return 'mdi-run-fast'
|
||||
}
|
||||
case 'ArmTest': {
|
||||
return 'mdi-arm-flex'
|
||||
}
|
||||
case 'BackTest': {
|
||||
return 'mdi-human-stretching'
|
||||
}
|
||||
case 'SitReachTest': {
|
||||
return 'mdi-human-stretching'
|
||||
}
|
||||
case 'UpGo8ft': {
|
||||
return 'mdi-navigation'
|
||||
}
|
||||
case 'OneLegTest': {
|
||||
return 'mdi-scale-balance'
|
||||
}
|
||||
default: {
|
||||
return 'mdi-checkbox-blank-circle-outline'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMeasurementSummary (code: string, vals: Record<string, any>) {
|
||||
switch (code) {
|
||||
case 'BloodPressure': {
|
||||
return `收縮壓: ${vals.systolic} / 舒張壓: ${vals.diastolic} mmHg | 心率: ${vals.heartRate} bpm`
|
||||
}
|
||||
case 'BodyFat': {
|
||||
return `BMI: ${vals.bmi} | 體脂率: ${vals.bodyFatPct}%`
|
||||
}
|
||||
case 'GripStrength': {
|
||||
return `左手: ${vals.leftHand} kg | 右手: ${vals.rightHand} kg`
|
||||
}
|
||||
case 'Sit5x': {
|
||||
return `耗時: ${vals.durationSeconds} 秒`
|
||||
}
|
||||
case 'Sit30s': {
|
||||
return `次數: ${vals.count} 次`
|
||||
}
|
||||
case 'StepTest': {
|
||||
return `踏步: ${vals.count} 次`
|
||||
}
|
||||
case 'ArmTest': {
|
||||
return `屈舉: ${vals.count} 次`
|
||||
}
|
||||
case 'BackTest': {
|
||||
return `左手: ${vals.leftDistanceCm} cm | 右手: ${vals.rightDistanceCm} cm`
|
||||
}
|
||||
case 'SitReachTest': {
|
||||
return `伸展: ${vals.distanceCm} cm`
|
||||
}
|
||||
case 'UpGo8ft': {
|
||||
return `耗時: ${vals.durationSeconds} 秒`
|
||||
}
|
||||
case 'OneLegTest': {
|
||||
return `左腳: ${vals.leftSeconds} 秒 | 右腳: ${vals.rightSeconds} 秒`
|
||||
}
|
||||
default: {
|
||||
return '數據登錄成功'
|
||||
}
|
||||
function getMeasurementSummary(code: string, vals: Record<string, any>) {
|
||||
switch (code) {
|
||||
case 'BloodPressure': {
|
||||
return `收縮壓: ${vals.systolic} / 舒張壓: ${vals.diastolic} mmHg | 心率: ${vals.heartRate} bpm`
|
||||
}
|
||||
case 'BodyFat': {
|
||||
return `BMI: ${vals.bmi} | 體脂率: ${vals.bodyFatPct}%`
|
||||
}
|
||||
case 'GripStrength': {
|
||||
return `左手: ${vals.leftHand} kg | 右手: ${vals.rightHand} kg`
|
||||
}
|
||||
case 'Sit5x': {
|
||||
return `耗時: ${vals.durationSeconds} 秒`
|
||||
}
|
||||
case 'Sit30s': {
|
||||
return `次數: ${vals.count} 次`
|
||||
}
|
||||
case 'StepTest': {
|
||||
return `踏步: ${vals.count} 次`
|
||||
}
|
||||
case 'ArmTest': {
|
||||
return `屈舉: ${vals.count} 次`
|
||||
}
|
||||
case 'BackTest': {
|
||||
return `左手: ${vals.leftDistanceCm} cm | 右手: ${vals.rightDistanceCm} cm`
|
||||
}
|
||||
case 'SitReachTest': {
|
||||
return `伸展: ${vals.distanceCm} cm`
|
||||
}
|
||||
case 'UpGo8ft': {
|
||||
return `耗時: ${vals.durationSeconds} 秒`
|
||||
}
|
||||
case 'OneLegTest': {
|
||||
return `左腳: ${vals.leftSeconds} 秒 | 右腳: ${vals.rightSeconds} 秒`
|
||||
}
|
||||
default: {
|
||||
return '數據登錄成功'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模擬一鍵完成所有測量 (對 Demo 极為有用)
|
||||
function simulateAllMeasurements () {
|
||||
// 自動將剩餘的 9 站填入合理健康的隨機值
|
||||
const m = store.measurements
|
||||
// 模擬一鍵完成所有測量 (對 Demo 极為有用)
|
||||
function simulateAllMeasurements() {
|
||||
// 自動將剩餘的 9 站填入合理健康的隨機值
|
||||
const m = store.measurements
|
||||
|
||||
if (!m.GripStrength.completed) {
|
||||
store.saveMeasurement('GripStrength', { leftHand: 32, rightHand: 34 })
|
||||
}
|
||||
if (!m.Sit5x.completed) {
|
||||
store.saveMeasurement('Sit5x', { durationSeconds: 8.5 })
|
||||
}
|
||||
if (!m.Sit30s.completed) {
|
||||
store.saveMeasurement('Sit30s', { count: 18 })
|
||||
}
|
||||
if (!m.StepTest.completed) {
|
||||
store.saveMeasurement('StepTest', { count: 92 })
|
||||
}
|
||||
if (!m.ArmTest.completed) {
|
||||
store.saveMeasurement('ArmTest', { count: 20 })
|
||||
}
|
||||
if (!m.BackTest.completed) {
|
||||
store.saveMeasurement('BackTest', { leftDistanceCm: 2, rightDistanceCm: 3 })
|
||||
}
|
||||
if (!m.SitReachTest.completed) {
|
||||
store.saveMeasurement('SitReachTest', { distanceCm: 14 })
|
||||
}
|
||||
if (!m.UpGo8ft.completed) {
|
||||
store.saveMeasurement('UpGo8ft', { durationSeconds: 5.8 })
|
||||
}
|
||||
if (!m.OneLegTest.completed) {
|
||||
store.saveMeasurement('OneLegTest', { leftSeconds: 24, rightSeconds: 26 })
|
||||
}
|
||||
if (!m.GripStrength.completed) {
|
||||
store.saveMeasurement('GripStrength', { leftHand: 32, rightHand: 34 })
|
||||
}
|
||||
|
||||
function generateReport () {
|
||||
store.generateFinalReport()
|
||||
reportSuccessDialog.value = true
|
||||
if (!m.Sit5x.completed) {
|
||||
store.saveMeasurement('Sit5x', { durationSeconds: 8.5 })
|
||||
}
|
||||
|
||||
function goToDashboard () {
|
||||
reportSuccessDialog.value = false
|
||||
router.push('/')
|
||||
if (!m.Sit30s.completed) {
|
||||
store.saveMeasurement('Sit30s', { count: 18 })
|
||||
}
|
||||
if (!m.StepTest.completed) {
|
||||
store.saveMeasurement('StepTest', { count: 92 })
|
||||
}
|
||||
if (!m.ArmTest.completed) {
|
||||
store.saveMeasurement('ArmTest', { count: 20 })
|
||||
}
|
||||
if (!m.BackTest.completed) {
|
||||
store.saveMeasurement('BackTest', { leftDistanceCm: 2, rightDistanceCm: 3 })
|
||||
}
|
||||
if (!m.SitReachTest.completed) {
|
||||
store.saveMeasurement('SitReachTest', { distanceCm: 14 })
|
||||
}
|
||||
if (!m.UpGo8ft.completed) {
|
||||
store.saveMeasurement('UpGo8ft', { durationSeconds: 5.8 })
|
||||
}
|
||||
if (!m.OneLegTest.completed) {
|
||||
store.saveMeasurement('OneLegTest', { leftSeconds: 24, rightSeconds: 26 })
|
||||
}
|
||||
}
|
||||
|
||||
function generateReport() {
|
||||
store.generateFinalReport()
|
||||
reportSuccessDialog.value = true
|
||||
}
|
||||
|
||||
function goToDashboard() {
|
||||
reportSuccessDialog.value = false
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<v-form @submit.prevent="handleSave">
|
||||
<h3 class="text-subtitle-2 font-weight-bold text-grey-darken-3 mb-3">數據錄入:</h3>
|
||||
|
||||
<v-row dense>
|
||||
<v-row>
|
||||
<v-col v-for="field in station.fields" :key="field.key" class="mb-4" cols="12">
|
||||
<div class="text-body-2 font-weight-bold text-grey-darken-3 mb-1">
|
||||
{{ field.label }} <span class="text-caption text-grey">({{ field.unit }})</span>
|
||||
|
||||
@@ -3,341 +3,105 @@
|
||||
<template #main>
|
||||
<template v-if="store.isLoggedIn">
|
||||
<!-- 頂部:我的健康力護照卡片 (Taipei Masters Hub) -->
|
||||
<v-card
|
||||
class="passport-card rounded-xl text-white pa-5 elevation-8 mb-6 position-relative overflow-hidden"
|
||||
>
|
||||
<!-- 背景裝飾圓圈 -->
|
||||
<div class="bg-circle-1" />
|
||||
<div class="bg-circle-2" />
|
||||
<v-card class="border-primary border-xl rounded-xl" variant="flat">
|
||||
<v-row class="mt-4" no-gutters>
|
||||
<v-col cols="8">
|
||||
<v-card-title> {{ store.userProfile.name }} 先生 </v-card-title>
|
||||
<v-card-subtitle> 資料更新於 {{ todayTimeString }} </v-card-subtitle>
|
||||
</v-col>
|
||||
|
||||
<div class="d-flex justify-space-between align-center mb-6 z-index-1 position-relative">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 border-white" color="accent" size="44">
|
||||
<span class="text-white font-weight-black text-subtitle-1">{{
|
||||
store.userProfile.name.substring(1)
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<h2 class="text-subtitle-1 font-weight-black mb-0">
|
||||
{{ store.userProfile.name }} 先生
|
||||
</h2>
|
||||
|
||||
<span class="text-caption text-white-70">更新於今日 {{ todayTimeString }}</span>
|
||||
<v-col class="d-flex justify-center" cols="4">
|
||||
<!-- 健康力分數大圓盤 -->
|
||||
<div class="score-disk d-flex flex-column justify-center align-center">
|
||||
<span class="score-val">{{ displayedHealthScore }}</span>
|
||||
<span class="score-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 健康力分數大圓盤 -->
|
||||
<div class="score-disk d-flex flex-column justify-center align-center">
|
||||
<span class="score-val">{{ store.healthScore }}</span>
|
||||
<span class="score-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能儲備與運動處方簡介 -->
|
||||
<div class="z-index-1 position-relative mb-4">
|
||||
<div class="d-inline-flex align-center bg-white-20 px-3 py-1 rounded-pill mb-3">
|
||||
<v-icon class="mr-1 text-accent-light" icon="mdi-heart-pulse" size="16" />
|
||||
<span class="text-caption font-weight-bold">功能儲備:{{ functionalReserve }}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-body-2 font-weight-medium bg-white-10 rounded-lg pa-3">
|
||||
<div class="font-weight-black mb-1 text-accent-light d-flex align-center">
|
||||
<v-icon class="mr-1" icon="mdi-notebook-check-outline" size="16" />
|
||||
本週處方建議:
|
||||
</div>
|
||||
{{ currentWeeklyPrescription }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="bg-white-20 my-3 z-index-1 position-relative" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 四項核心能力數值簡覽 -->
|
||||
<v-row class="z-index-1 position-relative text-center" dense>
|
||||
<v-col cols="3">
|
||||
<div class="text-caption text-white-70">肌力</div>
|
||||
<v-row class="mt-4" no-gutters>
|
||||
<v-col class="text-center border pa-2" cols="3">
|
||||
<div class="">肌力</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-black">
|
||||
<div class="font-weight-black">
|
||||
{{ store.radarData.strength }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="3">
|
||||
<div class="text-caption text-white-70">平衡</div>
|
||||
<v-col class="text-center border pa-2" cols="3">
|
||||
<div class="">平衡</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-black">
|
||||
<div class="font-weight-black">
|
||||
{{ store.radarData.balance }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="3">
|
||||
<div class="text-caption text-white-70">敏捷</div>
|
||||
<v-col class="text-center border pa-2" cols="3">
|
||||
<div class="">敏捷</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-black">
|
||||
<div class="font-weight-black">
|
||||
{{ store.radarData.agility }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="3">
|
||||
<div class="text-caption text-white-70">心肺</div>
|
||||
<v-col class="text-center border pa-2" cols="3">
|
||||
<div class="">心肺</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-black">
|
||||
<div class="font-weight-black">
|
||||
{{ store.radarData.cardio }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 功能儲備與運動處方簡介 -->
|
||||
|
||||
<v-card-text class="bg-primary">
|
||||
<div class="font-weight-bold text-accent-light">
|
||||
<v-icon icon="mdi-notebook-check-outline" size="16" />
|
||||
運動處方建議:
|
||||
</div>
|
||||
|
||||
<p class="text-white mt-2">
|
||||
{{ currentWeeklyPrescription }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 中間:功能指標與五維雷達圖 -->
|
||||
<v-card class="rounded-xl elevation-3 border-accent-light mb-6 pa-4" color="surface">
|
||||
<h3 class="text-subtitle-1 font-weight-black text-primary mb-2 d-flex align-center">
|
||||
<v-icon class="mr-2" color="secondary" icon="mdi-radar" />
|
||||
個人五維健康力指標
|
||||
</h3>
|
||||
<v-card class="border-xl border-primary rounded-xl mt-4" variant="flat">
|
||||
<v-card-title> 個人五維健康力指標 </v-card-title>
|
||||
|
||||
<p class="text-caption text-grey mb-4">
|
||||
綜合 11 項現場普測數值計算得出,藍色為同齡正常參考值區間。
|
||||
</p>
|
||||
<v-card-subtitle>
|
||||
綜合 11 項現場普測數值計算得出,<br />藍色為同齡正常參考值區間。
|
||||
</v-card-subtitle>
|
||||
|
||||
<!-- 純 SVG 雷達圖,不依賴任何外部圖表庫,保證完美渲染 -->
|
||||
<div class="d-flex justify-center my-2 position-relative">
|
||||
<svg class="radar-svg" height="260" viewBox="0 0 120 120" width="260">
|
||||
<!-- 5 個同心五邊形網格 (背景線) -->
|
||||
<polygon
|
||||
v-for="grid in [20, 40, 60, 80, 100]"
|
||||
:key="grid"
|
||||
fill="none"
|
||||
:points="getPolygonPoints(grid)"
|
||||
stroke="#E0E0E0"
|
||||
stroke-width="0.3"
|
||||
/>
|
||||
|
||||
<!-- 五個維度軸線 -->
|
||||
<line
|
||||
v-for="i in 5"
|
||||
:key="`axis-${i}`"
|
||||
stroke="#E0E0E0"
|
||||
stroke-width="0.3"
|
||||
x1="60"
|
||||
:x2="getAxisEndPoint(i - 1).x"
|
||||
y1="60"
|
||||
:y2="getAxisEndPoint(i - 1).y"
|
||||
/>
|
||||
|
||||
<!-- 同齡參考區間 (同齡常模基準:設為 65 分) -->
|
||||
<polygon
|
||||
fill="rgba(33, 150, 243, 0.12)"
|
||||
:points="getPolygonPoints(65)"
|
||||
stroke="#2196F3"
|
||||
stroke-dasharray="1 1"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
|
||||
<!-- 使用者真實數據填充面 -->
|
||||
<polygon
|
||||
fill="rgba(71, 194, 166, 0.35)"
|
||||
:points="userDataPoints"
|
||||
stroke="#1D7063"
|
||||
stroke-width="1.2"
|
||||
/>
|
||||
|
||||
<!-- 使用者數據頂點標記圓圈 -->
|
||||
<circle
|
||||
v-for="(pt, idx) in userDataCoordArray"
|
||||
:key="`dot-${idx}`"
|
||||
:cx="pt.x"
|
||||
:cy="pt.y"
|
||||
fill="#E66926"
|
||||
r="1.5"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
|
||||
<!-- 頂點文字標記 (Cardio, Strength, Balance, Flexibility, Agility) -->
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
x="60"
|
||||
y="8"
|
||||
>
|
||||
心肺 ({{ store.radarData.cardio }})
|
||||
</text>
|
||||
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="start"
|
||||
x="110"
|
||||
y="44"
|
||||
>
|
||||
肌力 ({{ store.radarData.strength }})
|
||||
</text>
|
||||
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="start"
|
||||
x="94"
|
||||
y="104"
|
||||
>
|
||||
平衡 ({{ store.radarData.balance }})
|
||||
</text>
|
||||
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="end"
|
||||
x="26"
|
||||
y="104"
|
||||
>
|
||||
柔軟 ({{ store.radarData.flexibility }})
|
||||
</text>
|
||||
|
||||
<text
|
||||
fill="#1D7063"
|
||||
font-size="4"
|
||||
font-weight="bold"
|
||||
text-anchor="end"
|
||||
x="10"
|
||||
y="44"
|
||||
>
|
||||
敏捷 ({{ store.radarData.agility }})
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
<!-- 圖例標示 -->
|
||||
<div class="radar-legend d-flex flex-column">
|
||||
<div class="d-flex align-center text-caption mb-1">
|
||||
<span class="legend-color bg-primary mr-1" />
|
||||
<span>我的指標</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center text-caption">
|
||||
<span class="legend-color bg-blue mr-1 border-dashed" />
|
||||
<span>同齡參考</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HealthRadarChart :radar-data="store.radarData" />
|
||||
</v-card>
|
||||
|
||||
<!-- 下一步行動引導 (根據量測進度動態顯示) -->
|
||||
<h3 class="text-subtitle-2 font-weight-black text-primary mb-3">下一步行動推薦</h3>
|
||||
<ModuleRouterCard
|
||||
description="完成線上自評,取得初步風險分級。"
|
||||
icon="mdi-clipboard-text-search"
|
||||
icon-color="accent"
|
||||
title="自評"
|
||||
to="/self-assessment"
|
||||
/>
|
||||
|
||||
<!-- 情況 1:普測量測正在進行中 -->
|
||||
<v-card
|
||||
v-if="isIncompleteMeasurement"
|
||||
class="rounded-xl elevation-3 mb-4 transition-card text-left"
|
||||
color="surface"
|
||||
<ModuleRouterCard
|
||||
description="完成數值紀錄,建立個人功能基線。"
|
||||
icon="mdi-run-fast"
|
||||
icon-color="accent"
|
||||
title="量測"
|
||||
to="/checkin"
|
||||
>
|
||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 text-white elevation-1" color="secondary" size="48">
|
||||
<v-icon icon="mdi-run-fast" />
|
||||
</v-avatar>
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
||||
繼續現場體能普測
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey">
|
||||
目前已完成 {{ completedCount }} / 11 站,繼續完成登錄。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 情況 2:量測全部完成,提醒可以看運動處方任務 -->
|
||||
<v-card
|
||||
v-else
|
||||
class="rounded-xl elevation-3 mb-4 transition-card text-left"
|
||||
color="surface"
|
||||
@click="prescriptionDialog = true"
|
||||
>
|
||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 text-white elevation-1" color="secondary" size="48">
|
||||
<v-icon icon="mdi-calendar-multiselect" />
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
||||
執行本週運動處方課程
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey">
|
||||
依普測結果產生,肌力 2 次、心肺 2 次、平衡 3 次。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 普測預約快捷卡片 -->
|
||||
<v-card
|
||||
class="rounded-xl elevation-3 mb-6 transition-card text-left"
|
||||
color="surface"
|
||||
<ModuleRouterCard
|
||||
description="預訂首次量測或回測,追蹤健康狀態。"
|
||||
icon="mdi-calendar-clock"
|
||||
icon-color="accent"
|
||||
title="預約"
|
||||
to="/appointment"
|
||||
>
|
||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar class="mr-3 text-white elevation-1" color="primary" size="48">
|
||||
<v-icon icon="mdi-calendar-clock" />
|
||||
</v-avatar>
|
||||
|
||||
<div>
|
||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
||||
預約 30 秒坐站與平衡回測
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey">4 週 / 12 週定期回測,動態追蹤體能進步狀態。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 最近一次量測結果摘要 -->
|
||||
<v-card class="rounded-xl elevation-3 mb-8 pa-4" color="surface">
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<h3 class="text-subtitle-1 font-weight-black text-primary d-flex align-center">
|
||||
<v-icon class="mr-2" color="secondary" icon="mdi-file-eye-outline" />
|
||||
最近量測數值摘要
|
||||
</h3>
|
||||
|
||||
<v-btn
|
||||
class="font-weight-bold px-0"
|
||||
color="primary"
|
||||
size="small"
|
||||
to="/checkin"
|
||||
variant="text"
|
||||
>查看全部</v-btn>
|
||||
</div>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="item in recentItems" :key="item.name" class="mb-2" cols="6">
|
||||
<div class="bg-grey-lighten-4 rounded-lg pa-3 h-100">
|
||||
<div class="text-caption text-grey-darken-1 mb-1">{{ item.name }}</div>
|
||||
|
||||
<div class="text-body-1 font-weight-black text-grey-darken-4">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
/>
|
||||
|
||||
<!-- 運動處方對話框 -->
|
||||
<v-dialog v-model="prescriptionDialog" max-width="500">
|
||||
@@ -417,17 +181,15 @@
|
||||
<template v-else>
|
||||
<div class="text-center mb-6">
|
||||
<v-avatar class="elevation-3 mb-4 border-accent-light" color="primary" size="72">
|
||||
<v-icon color="accent" icon="mdi-heart-pulse" size="42" />
|
||||
<v-icon color="red-accent-1" icon="mdi-heart-pulse" size="42" />
|
||||
</v-avatar>
|
||||
|
||||
<h1 class="text-h5 font-weight-black text-primary mb-2">運動玩轉健康力</h1>
|
||||
|
||||
<p class="text-body-2 text-grey-darken-1 mb-0">
|
||||
不登入也可以先完成線上健康自評,登入後可預約普測與查看量測紀錄。
|
||||
<p class="text-body-medium text-grey-darken-1 mb-0">
|
||||
不登入也可以先完成線上健康自評,<br />登入後可預約普測與查看量測紀錄。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-card class="rounded-xl elevation-3 border-accent-light mb-4" color="surface">
|
||||
<v-card class="rounded-xl elevation-2 border-accent-light mb-4" color="surface">
|
||||
<v-card-text class="pa-5">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-avatar class="mr-3 text-white" color="secondary" size="48">
|
||||
@@ -442,13 +204,13 @@
|
||||
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold mb-3"
|
||||
color="primary"
|
||||
prepend-icon="mdi-arrow-right-circle"
|
||||
class="rounded-lg font-weight-bold elevation-1 mb-3"
|
||||
color="secondary"
|
||||
prepend-icon="mdi-clipboard-text-search"
|
||||
to="/self-assessment"
|
||||
variant="flat"
|
||||
>
|
||||
開始健康自評
|
||||
開始自評
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
@@ -459,7 +221,7 @@
|
||||
to="/login"
|
||||
variant="outlined"
|
||||
>
|
||||
登入健康平台
|
||||
登入平台
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -483,116 +245,81 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import HealthRadarChart from '@/components/HealthRadarChart.vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
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
|
||||
|
||||
function animateHealthScore (targetScore: number) {
|
||||
if (healthScoreTimer !== undefined) {
|
||||
window.clearInterval(healthScoreTimer)
|
||||
}
|
||||
|
||||
if (targetScore <= 1) {
|
||||
displayedHealthScore.value = targetScore
|
||||
return
|
||||
}
|
||||
|
||||
displayedHealthScore.value = 1
|
||||
|
||||
const duration = 900
|
||||
const stepDuration = Math.max(12, Math.floor(duration / targetScore))
|
||||
|
||||
healthScoreTimer = window.setInterval(() => {
|
||||
if (displayedHealthScore.value >= targetScore) {
|
||||
window.clearInterval(healthScoreTimer)
|
||||
healthScoreTimer = undefined
|
||||
return
|
||||
}
|
||||
|
||||
displayedHealthScore.value += 1
|
||||
}, stepDuration)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
animateHealthScore(store.healthScore)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (healthScoreTimer !== undefined) {
|
||||
window.clearInterval(healthScoreTimer)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => store.healthScore,
|
||||
score => {
|
||||
animateHealthScore(score)
|
||||
},
|
||||
)
|
||||
|
||||
const todayTimeString = computed(() => {
|
||||
const now = new Date()
|
||||
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
})
|
||||
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')}`
|
||||
|
||||
// 功能儲備評估
|
||||
const functionalReserve = computed(() => {
|
||||
const score = store.healthScore
|
||||
if (score >= 80) return '良好 (無衰弱風險)'
|
||||
if (score >= 65) return '正常 (需持續運動維持)'
|
||||
return '待加強 (有衰弱與跌倒風險)'
|
||||
return `${date} ${time}`
|
||||
})
|
||||
|
||||
// 每週處方文字
|
||||
const currentWeeklyPrescription = computed(() => {
|
||||
if (store.healthScore >= 80) {
|
||||
return '本週處方:1次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
||||
return '1次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
||||
} else if (store.healthScore >= 65) {
|
||||
return '本週處方:2次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
||||
return '2次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
||||
} else {
|
||||
return '本週處方:3次防跌平衡引導 + 每日15分鐘扶椅坐站與慢走'
|
||||
return '3次防跌平衡引導 + 每日15分鐘扶椅坐站與慢走'
|
||||
}
|
||||
})
|
||||
|
||||
// 檢測是否還有未完成的測量站
|
||||
const isIncompleteMeasurement = computed(() => {
|
||||
return store.completedStationsCount < store.totalStationsCount
|
||||
})
|
||||
|
||||
const completedCount = computed(() => store.completedStationsCount)
|
||||
|
||||
// 最近一次量測結果摘要列表
|
||||
const recentItems = computed(() => {
|
||||
const m = store.measurements
|
||||
const items = []
|
||||
|
||||
if (m.BloodPressure.completed) {
|
||||
items.push({
|
||||
name: '血壓量測',
|
||||
value: `${m.BloodPressure.values.systolic}/${m.BloodPressure.values.diastolic} mmHg`,
|
||||
})
|
||||
}
|
||||
if (m.BodyFat.completed) {
|
||||
items.push({ name: '體組成 BMI', value: `${m.BodyFat.values.bmi}` })
|
||||
}
|
||||
if (m.GripStrength.completed) {
|
||||
items.push({ name: '右手最大握力', value: `${m.GripStrength.values.rightHand} kg` })
|
||||
} else {
|
||||
items.push({ name: '左右握力', value: '未測量' })
|
||||
}
|
||||
if (m.OneLegTest.completed) {
|
||||
items.push({
|
||||
name: '單腳站立平衡',
|
||||
value: `${Math.max(m.OneLegTest.values.leftSeconds, m.OneLegTest.values.rightSeconds)} 秒`,
|
||||
})
|
||||
} else {
|
||||
items.push({ name: '開眼單足立', value: '未測量' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// --- SVG 雷達圖點計算邏輯 ---
|
||||
const cx = 60
|
||||
const cy = 60
|
||||
const r = 45
|
||||
|
||||
// 計算 5 角多邊形的頂點
|
||||
// 頂點順序: i=0(心肺), i=1(肌力), i=2(平衡), i=3(柔軟), i=4(敏捷)
|
||||
function getAxisEndPoint (i: number, radius = r) {
|
||||
const angle = -Math.PI / 2 + (i * 2 * Math.PI) / 5
|
||||
return {
|
||||
x: cx + radius * Math.cos(angle),
|
||||
y: cy + radius * Math.sin(angle),
|
||||
}
|
||||
}
|
||||
|
||||
// 根據百分比半徑繪製背景網格五邊形
|
||||
function getPolygonPoints (percent: number) {
|
||||
const radius = (percent / 100) * r
|
||||
const pts = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const pt = getAxisEndPoint(i, radius)
|
||||
pts.push(`${pt.x},${pt.y}`)
|
||||
}
|
||||
return pts.join(' ')
|
||||
}
|
||||
|
||||
// 使用者五維數據對應的雷達圖頂點座標
|
||||
const userDataCoordArray = computed(() => {
|
||||
const data = store.radarData
|
||||
// 順序與頂點對應:
|
||||
// 0: cardio (心肺), 1: strength (肌力), 2: balance (平衡), 3: flexibility (柔軟), 4: agility (敏捷)
|
||||
const values = [data.cardio, data.strength, data.balance, data.flexibility, data.agility]
|
||||
return values.map((val, i) => {
|
||||
const radius = (Math.max(5, Math.min(100, val)) / 100) * r
|
||||
return getAxisEndPoint(i, radius)
|
||||
})
|
||||
})
|
||||
|
||||
const userDataPoints = computed(() => {
|
||||
return userDataCoordArray.value.map(pt => `${pt.x},${pt.y}`).join(' ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -681,31 +408,4 @@ const userDataPoints = computed(() => {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 雷達圖圖例 */
|
||||
.radar-legend {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.border-dashed {
|
||||
border: 1px dashed #2196f3;
|
||||
background-color: rgba(33, 150, 243, 0.12) !important;
|
||||
}
|
||||
|
||||
.radar-svg text {
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,13 +50,22 @@
|
||||
class="glow-sweep-button rounded-lg font-weight-bold elevation-2 mb-4"
|
||||
color="secondary"
|
||||
:loading="loading"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
登入健康平台
|
||||
</v-btn>
|
||||
</v-form>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg font-weight-bold"
|
||||
color="primary"
|
||||
prepend-icon="mdi-clipboard-text-search"
|
||||
to="/self-assessment"
|
||||
variant="outlined"
|
||||
>
|
||||
不登入,先做線上自評
|
||||
</v-btn>
|
||||
</v-form>
|
||||
<!-- <div class="d-flex align-center justify-center my-4">
|
||||
<v-divider class="mr-3" />
|
||||
<span class="text-caption text-grey font-weight-bold">或使用以下方式快捷登入</span>
|
||||
@@ -111,7 +120,7 @@ const password = ref('')
|
||||
const loading = ref(false)
|
||||
const lineLoading = ref(false)
|
||||
|
||||
function handleLogin () {
|
||||
function handleLogin() {
|
||||
if (!account.value) return
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
@@ -121,7 +130,7 @@ function handleLogin () {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function _handleLineLogin () {
|
||||
function _handleLineLogin() {
|
||||
lineLoading.value = true
|
||||
setTimeout(() => {
|
||||
store.login('0912-345-678') // LINE 模擬登入預設手機
|
||||
@@ -155,7 +164,7 @@ function _handleLineLogin () {
|
||||
rgba(255, 255, 255, 0.15) 65%,
|
||||
transparent 100%
|
||||
);
|
||||
content: "";
|
||||
content: '';
|
||||
transform: rotate(18deg);
|
||||
animation: glow-sweep 2.4s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
</div>
|
||||
|
||||
<!-- 進度指示器 -->
|
||||
<v-row class="mb-6 justify-center" dense>
|
||||
<v-row class="mb-6 justify-center">
|
||||
<v-col v-for="(stepName, idx) in stepNames" :key="idx" class="text-center" cols="3">
|
||||
<v-avatar
|
||||
class="text-white font-weight-bold mb-1 elevation-1"
|
||||
:color="currentStep > idx ? 'primary' : currentStep === idx ? 'secondary' : 'grey-lighten-2'"
|
||||
:color="
|
||||
currentStep > idx ? 'primary' : currentStep === idx ? 'secondary' : 'grey-lighten-2'
|
||||
"
|
||||
size="28"
|
||||
>
|
||||
<v-icon v-if="currentStep > idx" icon="mdi-check" size="14" />
|
||||
@@ -20,7 +22,13 @@
|
||||
|
||||
<div
|
||||
class="text-caption font-weight-bold"
|
||||
:class="currentStep === idx ? 'text-secondary' : currentStep > idx ? 'text-primary' : 'text-grey'"
|
||||
:class="
|
||||
currentStep === idx
|
||||
? 'text-secondary'
|
||||
: currentStep > idx
|
||||
? 'text-primary'
|
||||
: 'text-grey'
|
||||
"
|
||||
>
|
||||
{{ stepName }}
|
||||
</div>
|
||||
@@ -37,7 +45,7 @@
|
||||
基本資料確認
|
||||
</h2>
|
||||
|
||||
<v-row dense>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="profileForm.name"
|
||||
@@ -120,7 +128,9 @@
|
||||
活動習慣與想要改善的能力
|
||||
</h2>
|
||||
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">1. 最近 7 天,您每週進行大運動幾次?</p>
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||
1. 最近 7 天,您每週進行大運動幾次?
|
||||
</p>
|
||||
|
||||
<v-radio-group v-model="assessmentForm.exerciseFrequency" class="mb-4" color="primary">
|
||||
<v-card
|
||||
@@ -151,18 +161,26 @@
|
||||
</v-card>
|
||||
</v-radio-group>
|
||||
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">2. 您目前最想改善的身體功能? (可複選)</p>
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||
2. 您目前最想改善的身體功能? (可複選)
|
||||
</p>
|
||||
|
||||
<v-row dense>
|
||||
<v-row>
|
||||
<v-col v-for="imp in improvementsOptions" :key="imp" cols="6">
|
||||
<v-card
|
||||
class="rounded-lg py-2 px-1 text-center cursor-pointer transition-card"
|
||||
:class="{ 'bg-teal-lighten-5': assessmentForm.wantedImprovements.includes(imp) }"
|
||||
:color="assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey-lighten-1'"
|
||||
:color="
|
||||
assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey-lighten-1'
|
||||
"
|
||||
variant="outlined"
|
||||
@click="toggleImprovement(imp)"
|
||||
>
|
||||
<v-icon class="mb-1" :color="assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey'" :icon="getImprovementIcon(imp)" />
|
||||
<v-icon
|
||||
class="mb-1"
|
||||
:color="assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey'"
|
||||
:icon="getImprovementIcon(imp)"
|
||||
/>
|
||||
<div class="text-caption font-weight-bold">{{ imp }}</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -176,14 +194,18 @@
|
||||
身體限制與慢性病史
|
||||
</h2>
|
||||
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">1. 過去一年是否有過跌倒史?</p>
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||
1. 過去一年是否有過跌倒史?
|
||||
</p>
|
||||
|
||||
<v-radio-group v-model="assessmentForm.hasFalls" class="mb-4" color="primary" inline>
|
||||
<v-radio label="無跌倒" :value="false" />
|
||||
<v-radio label="有跌倒過" :value="true" />
|
||||
</v-radio-group>
|
||||
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">2. 目前身體關節疼痛狀況?</p>
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||
2. 目前身體關節疼痛狀況?
|
||||
</p>
|
||||
|
||||
<v-select
|
||||
v-model="assessmentForm.painLevel"
|
||||
@@ -194,9 +216,11 @@
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">3. 您有哪些慢性疾病? (可複選)</p>
|
||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||
3. 您有哪些慢性疾病? (可複選)
|
||||
</p>
|
||||
|
||||
<v-row class="mb-4" dense>
|
||||
<v-row class="mb-4">
|
||||
<v-col v-for="disease in diseaseOptions" :key="disease" cols="6">
|
||||
<v-checkbox
|
||||
v-model="assessmentForm.chronicDiseases"
|
||||
@@ -221,7 +245,10 @@
|
||||
@keyup.enter="addMedication"
|
||||
/>
|
||||
|
||||
<div v-if="assessmentForm.medicationReminders.length > 0" class="d-flex flex-wrap gap-2 mt-2">
|
||||
<div
|
||||
v-if="assessmentForm.medicationReminders.length > 0"
|
||||
class="d-flex flex-wrap gap-2 mt-2"
|
||||
>
|
||||
<v-chip
|
||||
v-for="(med, idx) in assessmentForm.medicationReminders"
|
||||
:key="idx"
|
||||
@@ -241,7 +268,10 @@
|
||||
<v-icon color="white" :icon="getResultIcon(resultLevel)" size="40" />
|
||||
</v-avatar>
|
||||
|
||||
<h2 class="text-h5 font-weight-bold mb-1" :class="`text-${getResultColor(resultLevel)}`">
|
||||
<h2
|
||||
class="text-h5 font-weight-bold mb-1"
|
||||
:class="`text-${getResultColor(resultLevel)}`"
|
||||
>
|
||||
自評分級:{{ resultLevel }}
|
||||
</h2>
|
||||
|
||||
@@ -254,7 +284,8 @@
|
||||
</div>
|
||||
|
||||
<p v-if="resultLevel === '低風險'" class="text-body-2 text-grey-darken-3 mb-2">
|
||||
您的身體功能與日常活動習慣良好。建議您每週繼續保持 3 次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動處方。
|
||||
您的身體功能與日常活動習慣良好。建議您每週繼續保持 3
|
||||
次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動處方。
|
||||
</p>
|
||||
|
||||
<p v-else-if="resultLevel === '需追蹤'" class="text-body-2 text-grey-darken-3 mb-2">
|
||||
@@ -266,7 +297,8 @@
|
||||
</p>
|
||||
|
||||
<div class="font-weight-bold text-caption text-secondary mt-3">
|
||||
* 禁忌提醒:若收縮壓 > 160mmHg、有關節強烈疼痛或暈眩時,請立即暫停所有體能檢測與訓練!
|
||||
* 禁忌提醒:若收縮壓 >
|
||||
160mmHg、有關節強烈疼痛或暈眩時,請立即暫停所有體能檢測與訓練!
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
@@ -310,13 +342,7 @@
|
||||
</v-btn>
|
||||
|
||||
<div v-else class="w-100 d-flex gap-2">
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg py-2"
|
||||
color="primary"
|
||||
to="/appointment"
|
||||
variant="flat"
|
||||
>
|
||||
<v-btn block class="rounded-lg py-2" color="primary" to="/appointment" variant="flat">
|
||||
立即預約普測
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -339,113 +365,120 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
const currentStep = ref(0)
|
||||
const stepNames = ['基本資料', '功能狀態', '健康限制', '自評結果']
|
||||
const store = useAppStore()
|
||||
const currentStep = ref(0)
|
||||
const stepNames = ['基本資料', '功能狀態', '健康限制', '自評結果']
|
||||
|
||||
const profileForm = reactive({
|
||||
name: store.userProfile.name,
|
||||
birthday: store.userProfile.birthday,
|
||||
gender: store.userProfile.gender,
|
||||
height: store.userProfile.height,
|
||||
weight: store.userProfile.weight,
|
||||
consent: store.userProfile.consent,
|
||||
const profileForm = reactive({
|
||||
name: store.userProfile.name,
|
||||
birthday: store.userProfile.birthday,
|
||||
gender: store.userProfile.gender,
|
||||
height: store.userProfile.height,
|
||||
weight: store.userProfile.weight,
|
||||
consent: store.userProfile.consent,
|
||||
})
|
||||
|
||||
const assessmentForm = reactive({
|
||||
exerciseFrequency: store.selfAssessment.exerciseFrequency,
|
||||
wantedImprovements: [...store.selfAssessment.wantedImprovements],
|
||||
hasFalls: store.selfAssessment.hasFalls,
|
||||
painLevel: store.selfAssessment.painLevel,
|
||||
chronicDiseases: [...store.selfAssessment.chronicDiseases],
|
||||
medicationReminders: [...store.selfAssessment.medicationReminders],
|
||||
})
|
||||
|
||||
const medicationInput = ref('')
|
||||
const resultLevel = ref(store.selfAssessment.scoreLevel)
|
||||
|
||||
const improvementsOptions = ['肌力', '平衡', '心肺', '柔軟度', '敏捷', '體脂/BMI']
|
||||
const diseaseOptions = ['高血壓', '糖尿病', '高血脂', '心臟病', '關節退化', '骨質疏鬆']
|
||||
|
||||
function getImprovementIcon(imp: string) {
|
||||
switch (imp) {
|
||||
case '肌力': {
|
||||
return 'mdi-arm-flex'
|
||||
}
|
||||
case '平衡': {
|
||||
return 'mdi-scale-balance'
|
||||
}
|
||||
case '心肺': {
|
||||
return 'mdi-heart-pulse'
|
||||
}
|
||||
case '柔軟度': {
|
||||
return 'mdi-human-stretching'
|
||||
}
|
||||
case '敏捷': {
|
||||
return 'mdi-run-fast'
|
||||
}
|
||||
case '體脂/BMI': {
|
||||
return 'mdi-calculator'
|
||||
}
|
||||
default: {
|
||||
return 'mdi-help-circle'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImprovement(imp: string) {
|
||||
const index = assessmentForm.wantedImprovements.indexOf(imp)
|
||||
if (index === -1) {
|
||||
assessmentForm.wantedImprovements.push(imp)
|
||||
} else {
|
||||
assessmentForm.wantedImprovements.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function addMedication() {
|
||||
if (medicationInput.value.trim()) {
|
||||
assessmentForm.medicationReminders.push(medicationInput.value.trim())
|
||||
medicationInput.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function removeMedication(index: number) {
|
||||
assessmentForm.medicationReminders.splice(index, 1)
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
// 儲存至 store
|
||||
store.updateProfile({
|
||||
name: profileForm.name,
|
||||
birthday: profileForm.birthday,
|
||||
gender: profileForm.gender,
|
||||
height: profileForm.height,
|
||||
weight: profileForm.weight,
|
||||
consent: profileForm.consent,
|
||||
})
|
||||
|
||||
const assessmentForm = reactive({
|
||||
exerciseFrequency: store.selfAssessment.exerciseFrequency,
|
||||
wantedImprovements: [...store.selfAssessment.wantedImprovements],
|
||||
hasFalls: store.selfAssessment.hasFalls,
|
||||
painLevel: store.selfAssessment.painLevel,
|
||||
chronicDiseases: [...store.selfAssessment.chronicDiseases],
|
||||
medicationReminders: [...store.selfAssessment.medicationReminders],
|
||||
store.submitSelfAssessment({
|
||||
exerciseFrequency: assessmentForm.exerciseFrequency,
|
||||
wantedImprovements: assessmentForm.wantedImprovements,
|
||||
hasFalls: assessmentForm.hasFalls,
|
||||
painLevel: assessmentForm.painLevel,
|
||||
chronicDiseases: assessmentForm.chronicDiseases,
|
||||
medicationReminders: assessmentForm.medicationReminders,
|
||||
})
|
||||
|
||||
const medicationInput = ref('')
|
||||
const resultLevel = ref(store.selfAssessment.scoreLevel)
|
||||
// 取得計算後的結果
|
||||
resultLevel.value = store.selfAssessment.scoreLevel
|
||||
currentStep.value = 3
|
||||
}
|
||||
|
||||
const improvementsOptions = ['肌力', '平衡', '心肺', '柔軟度', '敏捷', '體脂/BMI']
|
||||
const diseaseOptions = ['高血壓', '糖尿病', '高血脂', '心臟病', '關節退化', '骨質疏鬆']
|
||||
function getResultColor(lvl: string) {
|
||||
if (lvl === '低風險') return 'success'
|
||||
if (lvl === '需追蹤') return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
function getImprovementIcon (imp: string) {
|
||||
switch (imp) {
|
||||
case '肌力': { return 'mdi-arm-flex'
|
||||
}
|
||||
case '平衡': { return 'mdi-scale-balance'
|
||||
}
|
||||
case '心肺': { return 'mdi-heart-pulse'
|
||||
}
|
||||
case '柔軟度': { return 'mdi-human-stretching'
|
||||
}
|
||||
case '敏捷': { return 'mdi-run-fast'
|
||||
}
|
||||
case '體脂/BMI': { return 'mdi-calculator'
|
||||
}
|
||||
default: { return 'mdi-help-circle'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImprovement (imp: string) {
|
||||
const index = assessmentForm.wantedImprovements.indexOf(imp)
|
||||
if (index === -1) {
|
||||
assessmentForm.wantedImprovements.push(imp)
|
||||
} else {
|
||||
assessmentForm.wantedImprovements.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function addMedication () {
|
||||
if (medicationInput.value.trim()) {
|
||||
assessmentForm.medicationReminders.push(medicationInput.value.trim())
|
||||
medicationInput.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function removeMedication (index: number) {
|
||||
assessmentForm.medicationReminders.splice(index, 1)
|
||||
}
|
||||
|
||||
function handleSubmit () {
|
||||
// 儲存至 store
|
||||
store.updateProfile({
|
||||
name: profileForm.name,
|
||||
birthday: profileForm.birthday,
|
||||
gender: profileForm.gender,
|
||||
height: profileForm.height,
|
||||
weight: profileForm.weight,
|
||||
consent: profileForm.consent,
|
||||
})
|
||||
|
||||
store.submitSelfAssessment({
|
||||
exerciseFrequency: assessmentForm.exerciseFrequency,
|
||||
wantedImprovements: assessmentForm.wantedImprovements,
|
||||
hasFalls: assessmentForm.hasFalls,
|
||||
painLevel: assessmentForm.painLevel,
|
||||
chronicDiseases: assessmentForm.chronicDiseases,
|
||||
medicationReminders: assessmentForm.medicationReminders,
|
||||
})
|
||||
|
||||
// 取得計算後的結果
|
||||
resultLevel.value = store.selfAssessment.scoreLevel
|
||||
currentStep.value = 3
|
||||
}
|
||||
|
||||
function getResultColor (lvl: string) {
|
||||
if (lvl === '低風險') return 'success'
|
||||
if (lvl === '需追蹤') return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
function getResultIcon (lvl: string) {
|
||||
if (lvl === '低風險') return 'mdi-check-decagram'
|
||||
if (lvl === '需追蹤') return 'mdi-alert-decagram'
|
||||
return 'mdi-alert-octagon'
|
||||
}
|
||||
function getResultIcon(lvl: string) {
|
||||
if (lvl === '低風險') return 'mdi-check-decagram'
|
||||
if (lvl === '需追蹤') return 'mdi-alert-decagram'
|
||||
return 'mdi-alert-octagon'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -9,6 +9,9 @@ import { useAppStore } from '@/stores/app'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
scrollBehavior () {
|
||||
return { left: 0, top: 0 }
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
|
||||
@@ -44,7 +44,7 @@ export interface MeasurementStation {
|
||||
values: Record<string, any>
|
||||
}
|
||||
|
||||
function createDefaultUserProfile(): UserProfile {
|
||||
function createDefaultUserProfile (): UserProfile {
|
||||
return {
|
||||
name: '林建國',
|
||||
birthday: '1961-08-15',
|
||||
@@ -59,7 +59,7 @@ function createDefaultUserProfile(): UserProfile {
|
||||
}
|
||||
}
|
||||
|
||||
function createDefaultSelfAssessment(): SelfAssessment {
|
||||
function createDefaultSelfAssessment (): SelfAssessment {
|
||||
return {
|
||||
exerciseFrequency: '1-2',
|
||||
painLevel: '輕微',
|
||||
@@ -72,7 +72,7 @@ function createDefaultSelfAssessment(): SelfAssessment {
|
||||
}
|
||||
}
|
||||
|
||||
function createDefaultAppointments(): Appointment[] {
|
||||
function createDefaultAppointments (): Appointment[] {
|
||||
return [
|
||||
{
|
||||
id: 'apt-01',
|
||||
@@ -122,7 +122,7 @@ function createDefaultAppointments(): Appointment[] {
|
||||
]
|
||||
}
|
||||
|
||||
function createDefaultMeasurements(): Record<string, MeasurementStation> {
|
||||
function createDefaultMeasurements (): Record<string, MeasurementStation> {
|
||||
return {
|
||||
BloodPressure: {
|
||||
name: '血壓量測',
|
||||
@@ -184,7 +184,7 @@ function createDefaultMeasurements(): Record<string, MeasurementStation> {
|
||||
}
|
||||
}
|
||||
|
||||
function createDefaultRadarData() {
|
||||
function createDefaultRadarData () {
|
||||
return {
|
||||
strength: 65,
|
||||
balance: 60,
|
||||
@@ -205,7 +205,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
const isReportGenerated = ref(false)
|
||||
|
||||
const completedStationsCount = computed(() => {
|
||||
return Object.values(measurements.value).filter((station) => station.completed).length
|
||||
return Object.values(measurements.value).filter(station => station.completed).length
|
||||
})
|
||||
|
||||
const totalStationsCount = computed(() => {
|
||||
@@ -218,24 +218,24 @@ export const useAppStore = defineStore('app', () => {
|
||||
return 0
|
||||
}
|
||||
|
||||
const done = Object.values(measurements.value).filter((station) => station.completed).length
|
||||
const done = Object.values(measurements.value).filter(station => station.completed).length
|
||||
return Math.round((done / keys.length) * 100)
|
||||
})
|
||||
|
||||
function login(phone: string) {
|
||||
function login (phone: string) {
|
||||
isLoggedIn.value = true
|
||||
userProfile.value.phone = phone
|
||||
}
|
||||
|
||||
function logout() {
|
||||
function logout () {
|
||||
isLoggedIn.value = false
|
||||
}
|
||||
|
||||
function updateProfile(profile: Partial<UserProfile>) {
|
||||
function updateProfile (profile: Partial<UserProfile>) {
|
||||
userProfile.value = { ...userProfile.value, ...profile }
|
||||
}
|
||||
|
||||
function submitSelfAssessment(answers: Partial<SelfAssessment>) {
|
||||
function submitSelfAssessment (answers: Partial<SelfAssessment>) {
|
||||
selfAssessment.value = {
|
||||
...selfAssessment.value,
|
||||
...answers,
|
||||
@@ -248,9 +248,9 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
|
||||
if (
|
||||
answers.painLevel === '中度' ||
|
||||
answers.painLevel === '重度' ||
|
||||
(answers.chronicDiseases && answers.chronicDiseases.length >= 2)
|
||||
answers.painLevel === '中度'
|
||||
|| answers.painLevel === '重度'
|
||||
|| (answers.chronicDiseases && answers.chronicDiseases.length >= 2)
|
||||
) {
|
||||
level = '建議諮詢'
|
||||
}
|
||||
@@ -258,8 +258,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
selfAssessment.value.scoreLevel = level
|
||||
}
|
||||
|
||||
function registerAppointment(appointmentId: string) {
|
||||
const appointment = appointments.value.find((item) => item.id === appointmentId)
|
||||
function registerAppointment (appointmentId: string) {
|
||||
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||
if (!appointment) {
|
||||
return
|
||||
}
|
||||
@@ -268,8 +268,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
appointment.slots -= 1
|
||||
}
|
||||
|
||||
function cancelAppointment(appointmentId: string) {
|
||||
const appointment = appointments.value.find((item) => item.id === appointmentId)
|
||||
function cancelAppointment (appointmentId: string) {
|
||||
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||
if (!appointment) {
|
||||
return
|
||||
}
|
||||
@@ -278,8 +278,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
appointment.slots += 1
|
||||
}
|
||||
|
||||
function checkinAppointment(appointmentId: string) {
|
||||
const appointment = appointments.value.find((item) => item.id === appointmentId)
|
||||
function checkinAppointment (appointmentId: string) {
|
||||
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||
if (!appointment) {
|
||||
return
|
||||
}
|
||||
@@ -287,7 +287,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
appointment.status = '已報到'
|
||||
}
|
||||
|
||||
function saveMeasurement(code: string, values: Record<string, any>) {
|
||||
function saveMeasurement (code: string, values: Record<string, any>) {
|
||||
const measurement = measurements.value[code]
|
||||
if (!measurement) {
|
||||
return
|
||||
@@ -301,15 +301,15 @@ export const useAppStore = defineStore('app', () => {
|
||||
measurement.time = timeStr
|
||||
}
|
||||
|
||||
function generateFinalReport() {
|
||||
function generateFinalReport () {
|
||||
const currentMeasurements = measurements.value
|
||||
|
||||
let strScore = 65
|
||||
if (currentMeasurements.GripStrength.completed) {
|
||||
const avgGrip =
|
||||
((currentMeasurements.GripStrength.values.leftHand || 0) +
|
||||
(currentMeasurements.GripStrength.values.rightHand || 0)) /
|
||||
2
|
||||
const avgGrip
|
||||
= ((currentMeasurements.GripStrength.values.leftHand || 0)
|
||||
+ (currentMeasurements.GripStrength.values.rightHand || 0))
|
||||
/ 2
|
||||
const limit = userProfile.value.gender === '男' ? 30 : 20
|
||||
strScore = Math.min(95, Math.max(50, Math.round((avgGrip / limit) * 75)))
|
||||
}
|
||||
@@ -318,7 +318,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
if (currentMeasurements.OneLegTest.completed) {
|
||||
const maxSec = Math.max(
|
||||
currentMeasurements.OneLegTest.values.leftSeconds || 0,
|
||||
currentMeasurements.OneLegTest.values.rightSeconds || 0
|
||||
currentMeasurements.OneLegTest.values.rightSeconds || 0,
|
||||
)
|
||||
balScore = Math.min(98, Math.max(40, Math.round((maxSec / 30) * 90)))
|
||||
}
|
||||
@@ -354,7 +354,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
isReportGenerated.value = true
|
||||
}
|
||||
|
||||
function resetMeasurements() {
|
||||
function resetMeasurements () {
|
||||
for (const [key, measurement] of Object.entries(measurements.value)) {
|
||||
if (key !== 'BloodPressure' && key !== 'BodyFat') {
|
||||
measurement.completed = false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@font-face {
|
||||
font-family: "LINE Seed TW";
|
||||
font-family: "line-seed";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "LINE Seed TW";
|
||||
font-family: "line-seed";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "LINE Seed TW";
|
||||
font-family: "line-seed";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "LINE Seed TW";
|
||||
font-family: "line-seed";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
@@ -31,12 +31,12 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: "LINE Seed TW", sans-serif;
|
||||
font-family: "line-seed", sans-serif;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app,
|
||||
.v-application {
|
||||
font-family: "LINE Seed TW", sans-serif;
|
||||
font-family: "line-seed", sans-serif;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* Configures SASS variables and Vuetify overwrites
|
||||
*/
|
||||
|
||||
// https://vuetifyjs.com/features/sass-variables/`
|
||||
// @use 'vuetify/settings' with (
|
||||
// $color-pack: false
|
||||
// );
|
||||
// https://vuetifyjs.com/features/sass-variables/
|
||||
@use 'vuetify/settings' with (
|
||||
$body-font-family: ('line-seed', sans-serif),
|
||||
$heading-font-family: ('line-seed', sans-serif)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import Fonts from 'unplugin-fonts/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||
|
||||
@@ -17,17 +16,6 @@ export default defineConfig({
|
||||
configFile: 'src/styles/settings.scss',
|
||||
},
|
||||
}),
|
||||
Fonts({
|
||||
fontsource: {
|
||||
families: [
|
||||
{
|
||||
name: 'Roboto',
|
||||
weights: [100, 300, 400, 500, 700, 900],
|
||||
styles: ['normal', 'italic'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
define: { 'process.env': {} },
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user