255 lines
5.2 KiB
Vue
255 lines
5.2 KiB
Vue
|
|
<template>
|
||
|
|
<view class="stat-card" :class="{ 'is-loading': loading }">
|
||
|
|
<view class="card-header" v-if="title">
|
||
|
|
<text class="card-title">{{ title }}</text>
|
||
|
|
<uni-icons v-if="icon" :type="icon" size="20" :color="iconColor" />
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<view class="stats-grid" :class="gridClass">
|
||
|
|
<view
|
||
|
|
v-for="(item, index) in stats"
|
||
|
|
:key="index"
|
||
|
|
class="stat-item"
|
||
|
|
:class="{ 'clickable': item.clickable }"
|
||
|
|
@click="handleStatClick(item, index)">
|
||
|
|
|
||
|
|
<view class="stat-icon" v-if="item.icon" :style="{ backgroundColor: item.color }">
|
||
|
|
<uni-icons :type="item.icon" size="24" color="#fff" />
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<view class="stat-content">
|
||
|
|
<text class="stat-value" :style="{ color: item.color }">
|
||
|
|
{{ loading ? '--' : (item.value || 0) }}
|
||
|
|
</text>
|
||
|
|
<text class="stat-label">{{ item.label }}</text>
|
||
|
|
<text v-if="item.trend" class="stat-trend" :class="item.trend.type">
|
||
|
|
{{ item.trend.text }}
|
||
|
|
</text>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 加载骨架屏 -->
|
||
|
|
<view v-if="loading" class="loading-skeleton">
|
||
|
|
<view v-for="n in 3" :key="n" class="skeleton-item">
|
||
|
|
<view class="skeleton-circle"></view>
|
||
|
|
<view class="skeleton-lines">
|
||
|
|
<view class="skeleton-line"></view>
|
||
|
|
<view class="skeleton-line short"></view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
export default {
|
||
|
|
name: 'StatCard',
|
||
|
|
props: {
|
||
|
|
title: String,
|
||
|
|
icon: String,
|
||
|
|
iconColor: {
|
||
|
|
type: String,
|
||
|
|
default: '#666'
|
||
|
|
},
|
||
|
|
stats: {
|
||
|
|
type: Array,
|
||
|
|
default: () => []
|
||
|
|
},
|
||
|
|
loading: {
|
||
|
|
type: Boolean,
|
||
|
|
default: false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
computed: {
|
||
|
|
gridClass() {
|
||
|
|
return `cols-${this.stats.length}`
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
methods: {
|
||
|
|
handleStatClick(item, index) {
|
||
|
|
if (item.clickable) {
|
||
|
|
this.$emit('stat-click', { item, index })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="scss" scoped>
|
||
|
|
.stat-card {
|
||
|
|
background: #fff;
|
||
|
|
border-radius: 16rpx;
|
||
|
|
padding: 30rpx;
|
||
|
|
margin: 20rpx;
|
||
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
||
|
|
position: relative;
|
||
|
|
overflow: hidden;
|
||
|
|
|
||
|
|
&::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
height: 6rpx;
|
||
|
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 30rpx;
|
||
|
|
|
||
|
|
.card-title {
|
||
|
|
font-size: 32rpx;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.stats-grid {
|
||
|
|
display: flex;
|
||
|
|
|
||
|
|
&.cols-2 .stat-item {
|
||
|
|
flex: 0 0 50%;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.cols-3 .stat-item {
|
||
|
|
flex: 0 0 33.333%;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.cols-4 .stat-item {
|
||
|
|
flex: 0 0 25%;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-item {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
padding: 20rpx 10rpx;
|
||
|
|
border-radius: 12rpx;
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
|
||
|
|
&.clickable {
|
||
|
|
cursor: pointer;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
background: #f8f9fa;
|
||
|
|
transform: translateY(-2rpx);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-icon {
|
||
|
|
width: 60rpx;
|
||
|
|
height: 60rpx;
|
||
|
|
border-radius: 50%;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
margin-bottom: 16rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-content {
|
||
|
|
text-align: center;
|
||
|
|
|
||
|
|
.stat-value {
|
||
|
|
display: block;
|
||
|
|
font-size: 36rpx;
|
||
|
|
font-weight: 700;
|
||
|
|
line-height: 1.2;
|
||
|
|
margin-bottom: 8rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-label {
|
||
|
|
display: block;
|
||
|
|
font-size: 24rpx;
|
||
|
|
color: #666;
|
||
|
|
line-height: 1.2;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-trend {
|
||
|
|
display: block;
|
||
|
|
font-size: 20rpx;
|
||
|
|
margin-top: 4rpx;
|
||
|
|
|
||
|
|
&.up {
|
||
|
|
color: #52c41a;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.down {
|
||
|
|
color: #ff4d4f;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.stable {
|
||
|
|
color: #faad14;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 加载状态
|
||
|
|
&.is-loading {
|
||
|
|
.stats-grid {
|
||
|
|
opacity: 0.3;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.loading-skeleton {
|
||
|
|
position: absolute;
|
||
|
|
top: 80rpx;
|
||
|
|
left: 30rpx;
|
||
|
|
right: 30rpx;
|
||
|
|
display: flex;
|
||
|
|
|
||
|
|
.skeleton-item {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
margin: 0 10rpx;
|
||
|
|
|
||
|
|
.skeleton-circle {
|
||
|
|
width: 60rpx;
|
||
|
|
height: 60rpx;
|
||
|
|
border-radius: 50%;
|
||
|
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||
|
|
background-size: 200% 100%;
|
||
|
|
animation: loading 1.5s infinite;
|
||
|
|
margin-bottom: 16rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.skeleton-lines {
|
||
|
|
width: 100%;
|
||
|
|
|
||
|
|
.skeleton-line {
|
||
|
|
height: 24rpx;
|
||
|
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||
|
|
background-size: 200% 100%;
|
||
|
|
animation: loading 1.5s infinite;
|
||
|
|
border-radius: 4rpx;
|
||
|
|
margin-bottom: 8rpx;
|
||
|
|
|
||
|
|
&.short {
|
||
|
|
width: 60%;
|
||
|
|
margin: 0 auto;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes loading {
|
||
|
|
0% {
|
||
|
|
background-position: 200% 0;
|
||
|
|
}
|
||
|
|
100% {
|
||
|
|
background-position: -200% 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|