First commit
This commit is contained in:
94
frontend/src/components/common/StatCard.vue
Normal file
94
frontend/src/components/common/StatCard.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="stat-card">
|
||||
<div :class="['stat-icon', iconClass]">
|
||||
<component
|
||||
v-if="icon"
|
||||
:is="icon"
|
||||
class="w-6 h-6"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="stat-label truncate">{{ title }}</p>
|
||||
<div class="flex items-baseline gap-2 mt-1">
|
||||
<p class="stat-value">{{ formattedValue }}</p>
|
||||
<span
|
||||
v-if="change !== undefined"
|
||||
:class="['stat-trend', trendClass]"
|
||||
>
|
||||
<svg
|
||||
v-if="changeType !== 'neutral'"
|
||||
:class="['w-3 h-3', changeType === 'down' && 'rotate-180']"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{{ formattedChange }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
type ChangeType = 'up' | 'down' | 'neutral'
|
||||
type IconVariant = 'primary' | 'success' | 'warning' | 'danger'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
value: number | string
|
||||
icon?: Component
|
||||
iconVariant?: IconVariant
|
||||
change?: number
|
||||
changeType?: ChangeType
|
||||
formatValue?: (value: number | string) => string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
changeType: 'neutral',
|
||||
iconVariant: 'primary'
|
||||
})
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
if (props.formatValue) {
|
||||
return props.formatValue(props.value)
|
||||
}
|
||||
if (typeof props.value === 'number') {
|
||||
return props.value.toLocaleString()
|
||||
}
|
||||
return props.value
|
||||
})
|
||||
|
||||
const formattedChange = computed(() => {
|
||||
if (props.change === undefined) return ''
|
||||
const absChange = Math.abs(props.change)
|
||||
return `${absChange}%`
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
const classes: Record<IconVariant, string> = {
|
||||
primary: 'stat-icon-primary',
|
||||
success: 'stat-icon-success',
|
||||
warning: 'stat-icon-warning',
|
||||
danger: 'stat-icon-danger'
|
||||
}
|
||||
return classes[props.iconVariant]
|
||||
})
|
||||
|
||||
const trendClass = computed(() => {
|
||||
const classes: Record<ChangeType, string> = {
|
||||
up: 'stat-trend-up',
|
||||
down: 'stat-trend-down',
|
||||
neutral: 'text-gray-500 dark:text-dark-400'
|
||||
}
|
||||
return classes[props.changeType]
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user