PointsProgressRing.vue 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. <template>
  2. <view class="points-ring-container">
  3. <view class="ring-wrapper">
  4. <svg class="progress-ring" viewBox="0 0 120 120">
  5. <!-- Background ring -->
  6. <circle
  7. class="progress-ring__background"
  8. cx="60"
  9. cy="60"
  10. r="54"
  11. fill="none"
  12. stroke="#f0f9ff"
  13. stroke-width="10"
  14. />
  15. <!-- Progress ring -->
  16. <circle
  17. class="progress-ring__progress"
  18. cx="60"
  19. cy="60"
  20. r="54"
  21. fill="none"
  22. :stroke="progressColor"
  23. stroke-width="10"
  24. stroke-linecap="round"
  25. :stroke-dasharray="circumference"
  26. :stroke-dashoffset="dashOffset"
  27. />
  28. </svg>
  29. <view class="ring-content">
  30. <text class="points-value">{{ points }}</text>
  31. <text class="points-label">积分</text>
  32. </view>
  33. </view>
  34. <view class="milestone-info" v-if="milestone">
  35. <text class="milestone-text">🎯 距离下一个奖励还差 {{ milestone.remaining }} 分</text>
  36. </view>
  37. </view>
  38. </template>
  39. <script>
  40. export default {
  41. name: 'PointsProgressRing',
  42. props: {
  43. points: { type: Number, default: 0 },
  44. currentMilestone: { type: Number, default: 0 },
  45. nextMilestone: { type: Number, default: 100 }
  46. },
  47. computed: {
  48. circumference() {
  49. return 2 * Math.PI * 54
  50. },
  51. progress() {
  52. if (this.nextMilestone <= 0) return 0
  53. return Math.min(this.currentMilestone / this.nextMilestone, 1)
  54. },
  55. dashOffset() {
  56. return this.circumference * (1 - this.progress)
  57. },
  58. progressColor() {
  59. if (this.progress >= 0.75) return '#10B981' // green
  60. if (this.progress >= 0.5) return '#F59E0B' // yellow
  61. return '#0EA5E9' // blue
  62. },
  63. milestone() {
  64. if (this.nextMilestone > this.points) {
  65. return {
  66. remaining: this.nextMilestone - this.points,
  67. target: this.nextMilestone
  68. }
  69. }
  70. return null
  71. }
  72. }
  73. }
  74. </script>
  75. <style scoped>
  76. .points-ring-container {
  77. display: flex;
  78. flex-direction: column;
  79. align-items: center;
  80. }
  81. .ring-wrapper {
  82. position: relative;
  83. width: 120px;
  84. height: 120px;
  85. }
  86. .progress-ring {
  87. width: 100%;
  88. height: 100%;
  89. transform: rotate(-90deg);
  90. }
  91. .progress-ring__progress {
  92. transition: stroke-dashoffset 0.5s ease;
  93. }
  94. .ring-content {
  95. position: absolute;
  96. top: 50%;
  97. left: 50%;
  98. transform: translate(-50%, -50%);
  99. text-align: center;
  100. }
  101. .points-value {
  102. display: block;
  103. font-size: 28px;
  104. font-weight: 800;
  105. color: #0EA5E9;
  106. }
  107. .points-label {
  108. display: block;
  109. font-size: 12px;
  110. color: #94A3B8;
  111. }
  112. .milestone-info {
  113. margin-top: 8px;
  114. padding: 6px 12px;
  115. background: linear-gradient(135deg, #FEF3C7, #FDE68A);
  116. border-radius: 20px;
  117. }
  118. .milestone-text {
  119. font-size: 12px;
  120. color: #92400E;
  121. }
  122. </style>