Files
sports-empower-hub/app/citizen-frontend/src/components/HealthRadarChart.vue
T
skytek_xinliang b8997975a5 reformat
2026-10-05 10:11:04 +08:00

206 lines
4.4 KiB
Vue

<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(50)"
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.upperBodyStrength }})
</text>
<text
fill="#1D7063"
font-size="4"
font-weight="bold"
text-anchor="start"
x="94"
y="104"
>
下肢 ({{ radarData.lowerBodyStrengthAndEndurance }})
</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.balanceAgilityAndMobility }})
</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";
import type { PhysicalFitnessRadarData } from "@/utils/physical-fitness-radar";
const { radarData } = defineProps<{
radarData: PhysicalFitnessRadarData;
}>();
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.upperBodyStrength,
radarData.lowerBodyStrengthAndEndurance,
radarData.flexibility,
radarData.balanceAgilityAndMobility,
];
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;
}
.radar-svg {
overflow: visible;
}
</style>