| 1 | <template> |
| 2 | <n-card content-class="p-0!"> |
| 3 | <div class="stat-card flex flex-col items-center justify-center p-4 text-center"> |
| 4 | <div class="label text-xs" :style="{ color: 'var(--fg-secondary-color)' }">{{ label }}</div> |
| 5 | <div class="value mt-1 text-xl font-bold" :class="colorClass"> |
| 6 | {{ displayValue }} |
| 7 | </div> |
| 8 | </div> |
| 9 | </n-card> |
| 10 | </template> |
| 11 | |
| 12 | <script setup lang="ts"> |
| 13 | import { NCard } from "naive-ui" |
| 14 | import { computed, toRefs } from "vue" |
| 15 | |
| 16 | const props = withDefaults( |
| 17 | defineProps<{ |
| 18 | label: string |
| 19 | value: number | null | undefined |
| 20 | format?: "number" | "bytes" | "uptime" | "percent" |
| 21 | decimals?: number |
| 22 | colorThresholds?: { low: number; mid: number } |
| 23 | }>(), |
| 24 | { |
| 25 | format: "number", |
| 26 | decimals: 0 |
| 27 | } |
| 28 | ) |
| 29 | |
| 30 | const { label, value, format, decimals, colorThresholds } = toRefs(props) |
| 31 | |
| 32 | // TODO-FE: refactor |
| 33 | function formatBytes(bytes: number): string { |
| 34 | const units = ["B", "KB", "MB", "GB", "TB"] |
| 35 | let i = 0 |
| 36 | let v = bytes |
| 37 | while (v >= 1024 && i < units.length - 1) { |
| 38 | v /= 1024 |
| 39 | i++ |
| 40 | } |
| 41 | return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}` |
| 42 | } |
| 43 | |
| 44 | function formatUptime(seconds: number): string { |
| 45 | const d = Math.floor(seconds / 86400) |
| 46 | const h = Math.floor((seconds % 86400) / 3600) |
| 47 | const m = Math.floor((seconds % 3600) / 60) |
| 48 | if (d > 0) return `${d}d ${h}h ${m}m` |
| 49 | if (h > 0) return `${h}h ${m}m` |
| 50 | return `${m}m` |
| 51 | } |
| 52 | |
| 53 | const displayValue = computed(() => { |
| 54 | if (value.value === null || value.value === undefined) return "—" |
| 55 | const v = Number(value.value) |
| 56 | switch (format.value) { |
| 57 | case "bytes": |
| 58 | return formatBytes(v) |
| 59 | case "uptime": |
| 60 | return formatUptime(v) |
| 61 | case "percent": |
| 62 | return `${v.toFixed(decimals.value)}%` |
| 63 | default: |
| 64 | return v.toFixed(decimals.value) |
| 65 | } |
| 66 | }) |
| 67 | |
| 68 | const colorClass = computed(() => { |
| 69 | if (!colorThresholds?.value || value.value === null || value.value === undefined) return "" |
| 70 | const v = Number(value.value) |
| 71 | if (v < colorThresholds.value.low) return "text-red-400" |
| 72 | if (v < colorThresholds.value.mid) return "text-yellow-400" |
| 73 | return "text-green-400" |
| 74 | }) |
| 75 | </script> |