统计卡片网格组件
- 使用vue2+scss封装,有好看的动画效果
统计卡片网格组件
<template>
<div class="stat-grid-container">
<div
class="stat-grid"
:class="gridLayoutClass"
>
<div
v-for="(item, index) in items"
:key="item.id || index"
class="stat-card-wrapper"
>
<div
class="stat-card"
:style="{ backgroundColor: item.backgroundColor || getDefaultColor(index) }"
@click="handleCardClick(item, index)"
>
<div class="stat-icon">
<i
:class="getIconClass(item.icon)"
class="icon"
></i>
</div>
<div class="stat-content">
<div class="stat-title">{{ item.title }}</div>
<div class="stat-value">
<span class="value-number">{{ formatValue(item.value) }}</span>
<span class="value-unit">{{ item.unit || '个' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'StatCardGrid',
props: {
// 统计数据数组
items: {
type: Array,
required: true,
default: function() {
return []
}
},
// 固定间距大小 (当卡片数量 <= 4 时使用)
fixedGap: {
type: String,
default: '16px'
},
// 最小卡片宽度
minCardWidth: {
type: String,
default: '200px'
},
// 最大卡片宽度
maxCardWidth: {
type: String,
default: '300px'
},
// 是否格式化数值
formatNumber: {
type: Boolean,
default: true
}
},
data() {
return {
// Element UI 图标映射
iconMap: {
'file-text': 'el-icon-document',
'settings': 'el-icon-setting',
'home': 'el-icon-house',
'shield': 'el-icon-lock',
'file-check': 'el-icon-document-checked',
'database': 'el-icon-coin',
'users': 'el-icon-user-solid',
'trending-up': 'el-icon-top',
'alert-triangle': 'el-icon-warning',
'check-circle': 'el-icon-circle-check',
'search': 'el-icon-search',
'chart': 'el-icon-pie-chart',
'message': 'el-icon-message',
'phone': 'el-icon-phone',
'location': 'el-icon-location',
'time': 'el-icon-time'
},
// 默认颜色数组
defaultColors: [
'#6366f1', // 蓝色
'#f59e0b', // 橙色
'#06b6d4', // 青色
'#8b5cf6', // 紫色
'#ef4444', // 红色
'#6b7280' // 灰色
]
}
},
computed: {
// 计算网格布局类
gridLayoutClass() {
const itemCount = this.items.length
return {
'grid-fixed-spacing': itemCount <= 4,
'grid-space-between': itemCount > 4,
[`grid-cols-${Math.min(itemCount, 6)}`]: true
}
}
},
methods: {
// 获取图标类名
getIconClass(iconName) {
return this.iconMap[iconName] || 'el-icon-document'
},
// 获取默认颜色
getDefaultColor(index) {
return this.defaultColors[index % this.defaultColors.length]
},
// 格式化数值
formatValue(value) {
if (!this.formatNumber) return value
const num = Number(value)
if (isNaN(num)) return value
// 格式化大数字
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M'
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K'
}
return num.toLocaleString()
},
// 处理卡片点击事件
handleCardClick(item, index) {
this.$emit('card-click', { item, index })
}
}
}
</script>
<style lang="scss" scoped>
// SCSS 变量
$card-border-radius: 8px;
$card-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
$card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.15);
$transition-duration: 0.3s;
$icon-bg-opacity: 0.2;
// Mixins
@mixin card-hover-effect {
transform: translateY(-2px);
box-shadow: $card-shadow-hover;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// 主容器
.stat-grid-container {
width: 100%;
padding: 0;
}
// 网格容器
.stat-grid {
display: flex;
flex-wrap: wrap;
width: 100%;
align-items: stretch;
// 固定间距布局 (≤4个卡片)
&.grid-fixed-spacing {
gap: v-bind(fixedGap);
.stat-card-wrapper {
flex: 1;
min-width: v-bind(minCardWidth);
max-width: v-bind(maxCardWidth);
}
}
// space-between布局 (>4个卡片)
&.grid-space-between {
justify-content: space-between;
gap: 8px;
.stat-card-wrapper {
flex: 1;
min-width: 0;
max-width: calc((100% - 40px) / 6); // 最多6列,预留间距
}
}
}
// 响应式列数控制
@for $i from 1 through 6 {
.grid-cols-#{$i} .stat-card-wrapper {
@if $i == 1 {
min-width: 100%;
} @else if $i == 2 {
min-width: calc(50% - 8px);
} @else if $i == 3 {
min-width: calc(33.333% - 8px);
} @else if $i == 4 {
min-width: calc(25% - 12px);
} @else if $i == 5 {
min-width: calc(20% - 8px);
} @else if $i == 6 {
min-width: calc(16.666% - 8px);
}
}
}
// 卡片包装器
.stat-card-wrapper {
position: relative;
}
// 卡片样式
.stat-card {
border-radius: $card-border-radius;
box-shadow: $card-shadow;
padding: 16px;
height: 80px;
display: flex;
align-items: center;
gap: 12px;
color: white;
transition: all $transition-duration ease;
cursor: pointer;
position: relative;
overflow: hidden;
// 渐变遮罩
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.1) 100%);
pointer-events: none;
}
// 悬停效果
&:hover {
@include card-hover-effect;
}
// 激活效果
&:active {
transform: translateY(0);
}
}
// 图标容器
.stat-icon {
@include flex-center;
border-radius: $card-border-radius;
width: 48px;
height: 48px;
background-color: rgba(255, 255, 255, $icon-bg-opacity);
flex-shrink: 0;
transition: transform $transition-duration ease;
.icon {
color: white;
font-size: 20px;
}
// 图标悬停效果
.stat-card:hover & {
transform: scale(1.05);
}
}
// 内容区域
.stat-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
// 标题
.stat-title {
font-size: 14px;
font-weight: 500;
opacity: 0.9;
margin-bottom: 4px;
@include text-ellipsis;
}
// 数值容器
.stat-value {
display: flex;
align-items: baseline;
gap: 4px;
}
// 数值
.value-number {
font-size: 24px;
font-weight: bold;
line-height: 1;
font-family: 'Helvetica Neue', Arial, sans-serif;
}
// 单位
.value-unit {
font-size: 14px;
font-weight: 500;
opacity: 0.8;
}
// 响应式设计
@media (max-width: 1200px) {
.grid-space-between .stat-card-wrapper {
max-width: calc((100% - 32px) / 5);
}
.grid-cols-6 .stat-card-wrapper {
min-width: calc(20% - 8px);
}
}
@media (max-width: 992px) {
.grid-space-between .stat-card-wrapper {
max-width: calc((100% - 24px) / 4);
}
.grid-cols-5 .stat-card-wrapper,
.grid-cols-6 .stat-card-wrapper {
min-width: calc(25% - 8px);
}
}
@media (max-width: 768px) {
.stat-grid {
flex-direction: column;
gap: 12px !important;
.stat-card-wrapper {
min-width: 100% !important;
max-width: 100% !important;
}
}
.stat-card {
height: 70px;
padding: 14px;
.stat-icon {
width: 40px;
height: 40px;
.icon {
font-size: 18px;
}
}
.value-number {
font-size: 20px;
}
}
}
@media (max-width: 480px) {
.stat-card {
height: 60px;
padding: 12px;
gap: 10px;
.stat-icon {
width: 36px;
height: 36px;
.icon {
font-size: 16px;
}
}
.stat-title {
font-size: 12px;
}
.value-number {
font-size: 18px;
}
.value-unit {
font-size: 12px;
}
}
}
// 动画效果
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 可选的进入动画
.stat-card-wrapper {
animation: slideInUp 0.6s ease-out;
@for $i from 1 through 6 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 0.1}s;
}
}
}
</style>
使用示例
<template>
<div class="demo-container">
<!-- 控制面板 -->
<div class="control-panel">
<el-button @click="addCard" type="primary" icon="el-icon-plus">添加卡片</el-button>
<el-button @click="removeCard" type="danger" icon="el-icon-minus">移除卡片</el-button>
<el-button @click="resetCards" type="info" icon="el-icon-refresh">重置</el-button>
<span class="card-count">当前卡片数量: {{ currentItems.length }}</span>
</div>
<!-- 不同数量的演示 -->
<div class="demo-section">
<h3 class="section-title">
<i class="el-icon-data-line"></i>
动态卡片演示 ({{ currentItems.length }}个卡片 - {{ layoutType }})
</h3>
<StatCardGrid
:items="currentItems"
@card-click="handleCardClick"
:fixed-gap="'20px'"
/>
</div>
<!-- 固定演示 -->
<div class="demo-section">
<h3 class="section-title">
<i class="el-icon-data-board"></i>
6个卡片 (space-between布局)
</h3>
<StatCardGrid :items="allItems" @card-click="handleCardClick" />
</div>
<div class="demo-section">
<h3 class="section-title">
<i class="el-icon-data-analysis"></i>
4个卡片 (固定间距布局)
</h3>
<StatCardGrid :items="fourItems" @card-click="handleCardClick" />
</div>
<div class="demo-section">
<h3 class="section-title">
<i class="el-icon-pie-chart"></i>
2个卡片 (固定间距布局)
</h3>
<StatCardGrid :items="twoItems" @card-click="handleCardClick" />
</div>
</div>
</template>
<script>
import StatCardGrid from './StatCardGrid.vue'
export default {
name: 'StatGridDemo',
components: {
StatCardGrid
},
data() {
return {
// 所有卡片数据
allItems: [
{
id: 1,
title: '检测机构数',
value: 86,
unit: '个',
icon: 'file-text',
backgroundColor: '#6366f1'
},
{
id: 2,
title: '检测机构数',
value: 125,
unit: '个',
icon: 'settings',
backgroundColor: '#f59e0b'
},
{
id: 3,
title: '检测机构数',
value: 98,
unit: '个',
icon: 'home',
backgroundColor: '#06b6d4'
},
{
id: 4,
title: '检测机构数',
value: 156,
unit: '个',
icon: 'shield',
backgroundColor: '#8b5cf6'
},
{
id: 5,
title: '检测机构数',
value: 203,
unit: '个',
icon: 'file-check',
backgroundColor: '#ef4444'
},
{
id: 6,
title: '检测机构数',
value: 89,
unit: '个',
icon: 'database',
backgroundColor: '#6b7280'
}
],
// 当前显示的卡片
currentItems: []
}
},
computed: {
// 4个卡片
fourItems() {
return this.allItems.slice(0, 4)
},
// 2个卡片
twoItems() {
return this.allItems.slice(0, 2)
},
// 布局类型
layoutType() {
return this.currentItems.length <= 4 ? '固定间距' : 'space-between'
}
},
mounted() {
// 初始化显示3个卡片
this.currentItems = this.allItems.slice(0, 3)
},
methods: {
// 添加卡片
addCard() {
if (this.currentItems.length < this.allItems.length) {
this.currentItems.push(this.allItems[this.currentItems.length])
this.$message.success(`添加了第${this.currentItems.length}个卡片`)
} else {
this.$message.warning('已达到最大卡片数量')
}
},
// 移除卡片
removeCard() {
if (this.currentItems.length > 1) {
this.currentItems.pop()
this.$message.success(`移除了一个卡片,当前${this.currentItems.length}个`)
} else {
this.$message.warning('至少保留一个卡片')
}
},
// 重置卡片
resetCards() {
this.currentItems = this.allItems.slice(0, 3)
this.$message.info('已重置为3个卡片')
},
// 处理卡片点击
handleCardClick(data) {
this.$message({
message: `点击了: ${data.item.title} - ${data.item.value}${data.item.unit}`,
type: 'info'
})
}
}
}
</script>
<style lang="scss" scoped>
.demo-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.control-panel {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
.card-count {
font-weight: 500;
color: #606266;
margin-left: auto;
}
}
.demo-section {
margin-bottom: 40px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
border-left: 4px solid #6366f1;
padding-left: 12px;
display: flex;
align-items: center;
gap: 8px;
i {
color: #6366f1;
}
}
// 响应式
@media (max-width: 768px) {
.control-panel {
.card-count {
margin-left: 0;
width: 100%;
text-align: center;
margin-top: 10px;
}
}
}
</style>