192 lines
4.7 KiB
Vue
192 lines
4.7 KiB
Vue
|
|
<template>
|
||
|
|
<div
|
||
|
|
:class="[
|
||
|
|
'glass-stat-card group',
|
||
|
|
variant === 'ultra' ? 'glass-ultra' : 'glass-light',
|
||
|
|
'rounded-glass p-6 transition-all duration-300',
|
||
|
|
'hover:-translate-y-1 hover:shadow-glass-lg cursor-pointer'
|
||
|
|
]"
|
||
|
|
@click="$emit('click')"
|
||
|
|
>
|
||
|
|
<!-- Icon Section -->
|
||
|
|
<div
|
||
|
|
:class="[
|
||
|
|
'glass-stat-icon',
|
||
|
|
iconBgClass,
|
||
|
|
'w-14 h-14 rounded-2xl flex items-center justify-center mb-4',
|
||
|
|
'transition-transform duration-300 group-hover:scale-110'
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
<component
|
||
|
|
:is="icon"
|
||
|
|
:class="[iconColorClass, 'w-7 h-7']"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Content Section -->
|
||
|
|
<div class="space-y-2">
|
||
|
|
<!-- Label -->
|
||
|
|
<p class="text-sm font-medium text-gray-500 uppercase tracking-wider">
|
||
|
|
{{ label }}
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<!-- Value with Animation -->
|
||
|
|
<div class="flex items-baseline gap-2">
|
||
|
|
<CountUp
|
||
|
|
v-if="animated"
|
||
|
|
:end-val="numericValue"
|
||
|
|
:duration="2"
|
||
|
|
:prefix="prefix"
|
||
|
|
:suffix="suffix"
|
||
|
|
class="text-3xl font-bold text-gray-800"
|
||
|
|
/>
|
||
|
|
<p v-else class="text-3xl font-bold text-gray-800">
|
||
|
|
{{ prefix }}{{ value }}{{ suffix }}
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<!-- Change Indicator -->
|
||
|
|
<div
|
||
|
|
v-if="change"
|
||
|
|
:class="[
|
||
|
|
'flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium',
|
||
|
|
changeType === 'increase'
|
||
|
|
? 'bg-green-100 text-green-700'
|
||
|
|
: changeType === 'decrease'
|
||
|
|
? 'bg-red-100 text-red-700'
|
||
|
|
: 'bg-gray-100 text-gray-700'
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
<TrendingUp v-if="changeType === 'increase'" class="w-3 h-3" />
|
||
|
|
<TrendingDown v-else-if="changeType === 'decrease'" class="w-3 h-3" />
|
||
|
|
<span>{{ change }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Description -->
|
||
|
|
<p v-if="description" class="text-sm text-gray-600">
|
||
|
|
{{ description }}
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<!-- Progress Bar -->
|
||
|
|
<div v-if="showProgress" class="mt-3">
|
||
|
|
<div class="flex justify-between text-xs text-gray-500 mb-1">
|
||
|
|
<span>Progress</span>
|
||
|
|
<span>{{ progressValue }}%</span>
|
||
|
|
</div>
|
||
|
|
<div class="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
||
|
|
<div
|
||
|
|
:style="{ width: `${progressValue}%` }"
|
||
|
|
class="h-full bg-gradient-monaco rounded-full transition-all duration-500"
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Action Link -->
|
||
|
|
<div
|
||
|
|
v-if="actionLabel"
|
||
|
|
class="mt-4 pt-4 border-t border-white/40 flex items-center justify-between group/link"
|
||
|
|
>
|
||
|
|
<span class="text-sm font-medium text-monaco-600 group-hover/link:text-monaco-700">
|
||
|
|
{{ actionLabel }}
|
||
|
|
</span>
|
||
|
|
<ArrowRight class="w-4 h-4 text-monaco-600 transition-transform group-hover/link:translate-x-1" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { computed } from 'vue'
|
||
|
|
import { TrendingUp, TrendingDown, ArrowRight } from 'lucide-vue-next'
|
||
|
|
import CountUp from './CountUp.vue'
|
||
|
|
|
||
|
|
const props = defineProps({
|
||
|
|
icon: {
|
||
|
|
type: Object,
|
||
|
|
required: true
|
||
|
|
},
|
||
|
|
label: {
|
||
|
|
type: String,
|
||
|
|
required: true
|
||
|
|
},
|
||
|
|
value: {
|
||
|
|
type: [String, Number],
|
||
|
|
required: true
|
||
|
|
},
|
||
|
|
prefix: {
|
||
|
|
type: String,
|
||
|
|
default: ''
|
||
|
|
},
|
||
|
|
suffix: {
|
||
|
|
type: String,
|
||
|
|
default: ''
|
||
|
|
},
|
||
|
|
change: {
|
||
|
|
type: String,
|
||
|
|
default: null
|
||
|
|
},
|
||
|
|
changeType: {
|
||
|
|
type: String,
|
||
|
|
default: null,
|
||
|
|
validator: (value) => ['increase', 'decrease', 'neutral'].includes(value)
|
||
|
|
},
|
||
|
|
description: {
|
||
|
|
type: String,
|
||
|
|
default: null
|
||
|
|
},
|
||
|
|
actionLabel: {
|
||
|
|
type: String,
|
||
|
|
default: null
|
||
|
|
},
|
||
|
|
variant: {
|
||
|
|
type: String,
|
||
|
|
default: 'light',
|
||
|
|
validator: (value) => ['light', 'ultra'].includes(value)
|
||
|
|
},
|
||
|
|
iconColor: {
|
||
|
|
type: String,
|
||
|
|
default: 'monaco'
|
||
|
|
},
|
||
|
|
animated: {
|
||
|
|
type: Boolean,
|
||
|
|
default: true
|
||
|
|
},
|
||
|
|
showProgress: {
|
||
|
|
type: Boolean,
|
||
|
|
default: false
|
||
|
|
},
|
||
|
|
progressValue: {
|
||
|
|
type: Number,
|
||
|
|
default: 0
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
const emit = defineEmits(['click'])
|
||
|
|
|
||
|
|
const numericValue = computed(() => {
|
||
|
|
if (typeof props.value === 'number') return props.value
|
||
|
|
return parseFloat(props.value.replace(/[^0-9.-]/g, '')) || 0
|
||
|
|
})
|
||
|
|
|
||
|
|
const iconBgClass = computed(() => {
|
||
|
|
const colors = {
|
||
|
|
monaco: 'bg-glass-monaco-soft',
|
||
|
|
green: 'bg-green-50',
|
||
|
|
blue: 'bg-blue-50',
|
||
|
|
amber: 'bg-amber-50',
|
||
|
|
purple: 'bg-purple-50'
|
||
|
|
}
|
||
|
|
return colors[props.iconColor] || colors.monaco
|
||
|
|
})
|
||
|
|
|
||
|
|
const iconColorClass = computed(() => {
|
||
|
|
const colors = {
|
||
|
|
monaco: 'text-monaco-600',
|
||
|
|
green: 'text-green-600',
|
||
|
|
blue: 'text-blue-600',
|
||
|
|
amber: 'text-amber-600',
|
||
|
|
purple: 'text-purple-600'
|
||
|
|
}
|
||
|
|
return colors[props.iconColor] || colors.monaco
|
||
|
|
})
|
||
|
|
</script>
|