style: index page
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user