宜呗小程序--微信小程序
Lzk
2025-08-05 b7bcaf556aa2424e808b6e6426ab22df9cfe11c1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
<template>
  <view class="base-style"
        :style="'position: relative;width: ' + diameter + 'px;height: ' + diameter + 'px;display: flex;flex-direction: row;background-color: ' + bgColor + ';'">
    <!-- 左半圆和右半圆都要经历下面的5步:
    [第1步]第1层限定区域;
    [第2步]第2层决定显示一个整圆的左半边还是右半边;
    [第3步]第3层先使用激活颜色绘制一个圆环, 再添加一个同级且宽度为区域一半的盒子A;
    [第4步]在盒子A中再使用圆环底色绘制一个圆环, 此时整个圆环是 '左一半是激活颜色、右一半是圆环底色', 但这个圆环同时只能被看到一半;
    [第5步]旋转第2层。 -->
 
    <!-- 左半圆 -->
    <view class="base-style" :style="firstLayerViewStyle">
      <view :style="secondLayerViewStyle + secondLayerForLeft">
        <!-- 使用激活颜色绘制一个圆环。 -->
        <view :style="thirdLayerStyle">
        </view>
        <!-- 再使用背景色遮盖同级圆环的一半。 -->
        <view class="base-style" :style="thirdLayerStyleForBg">
          <view :style="fourthLayerStyleForBg" />
        </view>
        <view v-if="0 < ePercent && ePercent < 0.5" :style="endPointStyle + endPointStyleForLeft" />
      </view>
    </view>
 
    <!-- 右半圆 -->
    <view class="base-style" :style="firstLayerViewStyle">
      <!-- 适配:为了避免右侧遮盖显示不全 此处向左多移动了1px -->
      <view :style="secondLayerViewStyle + 'left: ' + (- diameter / 2 - 1) + 'px;' + secondLayerForRight">
        <!-- 使用激活颜色绘制一个圆环。 -->
        <view :style="thirdLayerStyle">
        </view>
        <!-- 再使用背景色遮盖同级圆环的一半。 -->
        <view class="base-style" :style="thirdLayerStyleForBg">
          <view :style="fourthLayerStyleForBg" />
        </view>
        <view v-if="ePercent > 0.5" :style="endPointStyle + endPointStyleForRight" />
      </view>
    </view>
 
    <view v-if="0.5 == ePercent" :style="endPointStyle + 'background-color: ' + this.hoopBgColor + ';'" />
    <!-- #ifdef APP-PLUS -->
    <!-- 处理现象: 安卓App的顶部和底部会有一个小白点。 -->
    <!-- <view v-if="ePercent > 0.5" :style="'position: absolute;top: 0;' + repairPointStyle" /> -->
    <!-- <view v-if="1.0 == ePercent" :style="'position: absolute;bottom: 0;' + repairPointStyle" /> -->
    <!-- #endif -->
  </view>
</template>
 
<script>
export default {
  name: 'progressCircle',
  props: {
    // 背景色(不宜设置为透明 否则 需要 在 左thirdLayer 的外面 再嵌套一个盒子)。
    bgColor: {
      type: String,
      default: '#FFFFFF'
    },
    // 圆环的外直径(单位px)。
    diameter: {
      type: Number,
      default: 250
    },
    // 圆环线条的厚度(单位px)。
    hoopThickness: {
      type: Number,
      default: 8
    },
    // 圆环底色(灰色的圆环)。
    hoopBgColor: {
      type: String,
      default: '#D1D1D1'
    },
    // 圆环激活部分的颜色。
    hoopColor: {
      type: String,
      default: '#FF4C20'
    },
    // 圆环进度百分比值(其值范围在0到1之间)。
    percent: {
      type: [Number, String],
      default: 0,
      validator: val => {
        return val >= 0 && val <= 1;
      },
    },
    animate: {
      type: Boolean,
      default: false,
    },
    // 动画持续时间(单位:毫秒)
    animationDuration: {
      type: Number,
      default: 2000
    },
    // 动画步进值
    animationStep: {
      type: Number,
      default: 0.1
    }
  },
  data() {
    return {
      targetPercent: 0,
      ePercent: 0,
      showTimer: undefined,
    };
  },
  watch: {
    percent: {
      handler: function() {
        console.log('progressCircle_watch_percent', this.percent);
        this.loadData();
      },
    },
  },
  computed: {
    firstLayerViewStyle() {
      return 'position: relative;width: ' + (this.diameter / 2) +
          'px;height: ' + this.diameter + 'px;';
    },
    secondLayerViewStyle() {
      return 'box-sizing: border-box;position: absolute;top: 0;width: ' + this.diameter +
          'px;height: ' + this.diameter + 'px;';
    },
    thirdLayerStyle() {
      return 'box-sizing: border-box;width: ' + this.diameter + 'px;height: ' + this.diameter +
          'px;border-radius: ' + (this.diameter / 2) +
          'px;border-width: ' + this.hoopThickness +
          'px;border-style: solid;border-color: ' + this.hoopColor + ';';
    },
    thirdLayerStyleForBg() {
      return 'box-sizing: border-box;position: absolute;top: 0;left: ' + (this.diameter / 2) + 'px;width: ' +
          this.diameter + 'px;height: ' + this.diameter + 'px;background-color: ' + this.bgColor + ';';
    },
    fourthLayerStyleForBg() {
      return 'box-sizing: border-box;margin-left: ' + (-this.diameter / 2) + 'px;width: ' + this.diameter +
          'px;height: ' +
          this.diameter + 'px;border-radius: ' + (this.diameter / 2) + 'px;border-width: ' +
          this.hoopThickness + 'px;border-style: solid;border-color: ' + this.hoopBgColor + ';';
    },
    secondLayerForLeft() {
      let angle = 0;
      if (this.ePercent < 0.5) {
        angle += (180 * (this.ePercent - 0.5) / 0.5);
      }
      // #ifdef APP-PLUS
      return 'left: 0;transform: rotate(' + angle + 'deg);';
      // #endif
      // #ifndef APP-PLUS
      return 'left: 0;transform: rotate(' + angle + 'deg);-webkit-transform: rotate(' + angle + 'deg);';
      // #endif
    },
    secondLayerForRight() {
      let angle = 0;
      if (this.ePercent > 0.5) {
        angle += (180 * (this.ePercent - 0.5) / 0.5);
      }
      // #ifdef APP-PLUS
      return 'right: 0;transform: rotate(' + angle + 'deg);';
      // #endif
      // #ifndef APP-PLUS
      return 'right: 0;transform: rotate(' + angle + 'deg);-webkit-transform: rotate(' + angle + 'deg);';
      // #endif
    },
    // repairPointStyle() {
    //     return 'left: ' + (this.diameter - this.hoopThickness) / 2 + 'px;width: ' +
    //         this.hoopThickness + 'px;height: ' + this.hoopThickness + 'px;border-radius: ' +
    //         this.hoopThickness / 2 + 'px;background-color: ' + this.hoopColor + ';';
    // },
    endPointStyle() {
      // 结束点圆心圈直径。
      const _circleCenterRadius = 2;
      return 'box-sizing: border-box;position: absolute;top: 0;left: ' + (this.diameter - this.hoopThickness) / 2 +
          'px;width: ' +
          this.hoopThickness + 'px;height: ' + this.hoopThickness + 'px;border-radius: ' + (this.hoopThickness / 2) +
          'px;border-width: ' + (this.hoopThickness / 2 - _circleCenterRadius) +
          'px;border-style: solid;border-color: ' +
          this.hoopColor + ';';
    },
    endPointStyleForLeft() {
      return 'background-color: ' + ((this.ePercent > 0.5) ? this.hoopColor : this.hoopBgColor) + ';';
    },
    endPointStyleForRight() {
      return 'background-color: ' + ((1 == this.ePercent) ? this.hoopColor : this.hoopBgColor) + ';';
    },
  },
  mounted() {
    console.log('progressCircle_mounted');
    this.loadData();
  },
  methods: {
    loadData() {
      this.targetPercent = parseFloat(this.percent);
      console.log('progressCircle_loadData');
      if (!this.animate) {
        this.ePercent = this.targetPercent;
      } else {
        let _this = this;
        this.ePercent = 0;
        this.showTimer && clearInterval(this.showTimer);
        
        // 计算动画间隔时间
        const stepInterval = Math.max(50, this.animationDuration * this.animationStep);
        
        this.showTimer = setInterval(() => {
          let tempPercent = _this.ePercent + _this.animationStep;
          if (tempPercent < _this.targetPercent) {
            _this.ePercent = tempPercent;
            return;
          };
          _this.ePercent = _this.targetPercent;
          clearInterval(_this.showTimer);
        }, stepInterval);
      }
    }
  }
}
</script>
 
<style scoped>
.base-style {
  box-sizing: border-box;
  /* 溢出隐藏 */
  overflow: hidden;
}
</style>