monacousa-portal/components/GlassStatCard.vue

192 lines
4.7 KiB
Vue
Raw Normal View History

<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>