TotalPointsDisplay.vue 4.72 KB
<template>
  <view class="total-points-display-container">
    <!-- 头部slot -->
    <view v-if="$slots.header" class="total-points-display-header">
      <slot name="header"></slot>
    </view>

    <!-- 积分显示器主体 -->
    <view class="total-points-display" :style="{ height: height }">
      <!-- 中心圆形显示总积分 -->
      <view class="center-circle1">
        <view class="total-points" @tap="handleGoToRewards">
          <text v-if="!isOwner" class="family-points-label">家庭总积分</text>
          <text class="points-number" :style="{ fontSize: dynamicFontSize }">{{ animatedTotalPoints }}分</text>
          <text v-if="isOwner" class="points-label">去兑换</text>
        </view>
      </view>
    </view>

    <!-- 底部slot -->
    <view v-if="$slots.footer" class="total-points-display-footer">
      <slot name="footer"></slot>
    </view>
  </view>
</template>

<script setup>
import { ref, watch, onMounted, defineProps, computed } from 'vue'
import Taro from '@tarojs/taro'

// 定义props
const props = defineProps({
  totalPoints: {
    type: Number,
    required: true,
    default: 0
  },
  height: {
    type: String,
    default: '30vh'
  },
  isOwner: {
    type: Boolean,
    default: false
  }
})

// 响应式数据
const animatedTotalPoints = ref(0)

/**
 * 根据数值长度动态计算字体大小
 */
const dynamicFontSize = computed(() => {
  const pointsStr = animatedTotalPoints.value + '分'
  const length = pointsStr.length

  // 基础字体大小36rpx,超过6位数开始缩小
  if (length <= 6) {
    return '36rpx'
  } else if (length <= 8) {
    return '32rpx'
  } else if (length <= 10) {
    return '28rpx'
  } else {
    return '24rpx'
  }
})

/**
 * 数字滚动动画
 * @param {number} start - 动画开始值
 * @param {number} end - 动画结束值
 */
const animateNumber = (start, end) => {
  if (start === end) {
    animatedTotalPoints.value = end;
    return;
  }
  const duration = 800;
  const startTime = Date.now();
  const difference = end - start;

  const animate = () => {
    const elapsed = Date.now() - startTime;
    const progress = Math.min(elapsed / duration, 1);
    const easeOut = 1 - Math.pow(1 - progress, 3);
    animatedTotalPoints.value = Math.floor(start + difference * easeOut);

    if (progress < 1) {
      requestAnimationFrame(animate);
    } else {
      animatedTotalPoints.value = end;
    }
  };
  animate();
}

// 监听totalPoints变化,并执行动画
watch(() => props.totalPoints, (newValue, oldValue) => {
  animateNumber(oldValue, newValue);
})

onMounted(() => {
    animateNumber(0, props.totalPoints)
})

/**
 * 处理去兑换点击事件
 */
const handleGoToRewards = () => {
  if (!props.isOwner) {
    return
  }
  Taro.navigateTo({
    url: '/pages/RewardCategories/index',
  })
}

/**
 * 处理积分规则点击事件
 */
const handleGoToPointsRule = () => {
  Taro.navigateTo({
    url: '/pages/PointsList/index'
  })
}
</script>

<style lang="less">
.total-points-display-container {
  background: white;
  border-radius: 24rpx;
  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
  margin: 32rpx;
  overflow: hidden;
}

.total-points-display-header {
  padding: 40rpx;
  padding-bottom: 20rpx;
}

.total-points-display-footer {
  padding: 40rpx;
  padding-top: 20rpx;
}

.total-points-display {
  position: relative;
  width: 100%;
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
}

// .points-rule-tip {
//   position: absolute;
//   // background-color: white;
//   top: 0rpx;
//   right: 10rpx;
//   display: flex;
//   flex-direction: column;
//   align-items: center;
//   justify-content: center;
//   z-index: 999;
//   cursor: pointer;
// }

// .tip-icon {
//   font-size: 32rpx;
//   margin-bottom: 4rpx;
// }

// .tip-text {
//   font-size: 20rpx;
//   color: #666;
//   line-height: 1;
// }

.center-circle1 {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 170rpx;
  height: 170rpx;
  background: linear-gradient(135deg, var(--primary-color), var(--primary-color));
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 8rpx 32rpx rgba(53, 144, 255, 0.3);
  z-index: 10;
}

.total-points {
  text-align: center;
  color: white;
}

.points-number {
  display: block;
  font-size: 48rpx;
  font-weight: bold;
  line-height: 1;
}

.family-points-label {
  display: block;
  font-size: 24rpx;
  margin-bottom: 10rpx;
  opacity: 0.9;
}

.points-label {
  display: block;
  font-size: 24rpx;
  margin-top: 10rpx;
  opacity: 0.9;
}

// 响应式适配
@media screen and (max-width: 750px) {
  .center-circle {
    width: 160rpx;
    height: 160rpx;
  }

  .points-number {
    font-size: 36rpx;
  }

  .points-label {
    font-size: 20rpx;
  }
}
</style>