统计卡片网格组件

0 阅读4分钟

统计卡片网格组件

  • 使用vue2+scss封装,有好看的动画效果

image.png


统计卡片网格组件

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