dengjunjie
5 天以前 4f39dcc195f28fa275fc2d065fbf1bf6a46c21b7
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
<template>
  <view
    :id="elId"
    class="tn-rate-class tn-rate"
    @touchmove.stop.prevent="touchMove"
  >
    <view class="tn-rate__wrap" :class="[elClass]" v-for="(item, index) in count" :key="index">
      <view
        class="tn-rate__wrap__icon"
        :class="[`tn-icon-${(allowHalf && halfIcon ? activeIndex > index + 1 : activeIndex > index) ? elActionIcon : elInactionIcon}`]"
        :style="[iconStyle(index)]"
        @tap="click(index + 1, $event)"
      >
        <!-- 半图标 -->
        <view
          v-if="showHalfIcon(index)"
          class="tn-rate__wrap__icon--half"
          :class="[`tn-icon-${elActionIcon}`]"
          :style="[halfIconStyle]"
        ></view>
      </view>
    </view>
  </view>
</template>
 
<script>
  export default {
    name: 'tn-rate',
    props: {
      // 选中星星的数量
      value: {
        type: Number,
        default: 0
      },
      // 显示的星星数
      count: {
        type: Number,
        default: 5
      },
      // 最小能选择的星星数
      minCount: {
        type: Number,
        default: 0
      },
      // 禁用状态
      disabled: {
        type: Boolean,
        default: false
      },
      // 是否可以选择半星
      allowHalf: {
        type: Boolean,
        default: false
      },
      // 星星大小
      size: {
        type: Number,
        default: 32
      },
      // 被选中的图标
      activeIcon: {
        type: String,
        default: 'star-fill'
      },
      // 未被选中的图标
      inactiveIcon: {
        type: String,
        default: 'star'
      },
      // 被选中的颜色
      activeColor: {
        type: String,
        default: '#01BEFF'
      },
      // 默认颜色
      inactiveColor: {
        type: String,
        default: '#AAAAAA'
      },
      // 星星之间的距离
      gutter: {
        type: Number,
        default: 10
      },
      // 自定义颜色
      colors: {
        type: Array,
        default() {
          return []
        }
      },
      // 自定义图标
      icons: {
        type: Array,
        default() {
          return []
        }
      }
    },
    computed: {
      // 图标显示的比例
      showHalfIcon(index) {
        return index => {
          return this.allowHalf && Math.ceil(this.activeIndex) === index + 1 && this.halfIcon
        }
      },
      // 被激活的图标
      elActionIcon() {
        const len = this.icons.length
        // icons参数传递了3个图标,当选中2个时,用第一个图标,4个时,用第二个图标
        // 5个时,用第三个图标作为激活的图标
        if (len && len <= this.count) {
          const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
          if (step < 1) return this.icons[0]
          if (step > len) return this.icons[len - 1]
          return this.icons[step - 1]
        }
        return this.activeIcon
      },
      // 未被激活的图标
      elInactionIcon() {
        const len = this.icons.length
        // icons参数传递了3个图标,当选中2个时,用第一个图标,4个时,用第二个图标
        // 5个时,用第三个图标作为激活的图标
        if (len && len <= this.count) {
          const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
          if (step < 1) return this.icons[0]
          if (step > len) return this.icons[len - 1]
          return this.icons[step - 1]
        }
        return this.inactiveIcon
      },
      // 被激活的颜色
      elActionColor() {
        const len = this.colors.length
        // 如果有设置colors参数(此参数用于将图标分段,比如一共5颗星,colors传3个颜色值,那么根据一定的规则,2颗星可能为第一个颜色
        // 4颗星为第二个颜色值,5颗星为第三个颜色值)
        if (len && len <= this.count) {
          const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
          if (step < 1) return this.colors[0]
          if (step > len) return this.colors[len - 1]
          return this.colors[step - 1]
        }
        return this.activeColor
      },
      // 图标的样式
      iconStyle() {
        return index => {
          let style = {}
          
          style.fontSize = this.$t.string.getLengthUnitValue(this.size)
          style.padding = `0 ${this.$t.string.getLengthUnitValue(this.gutter / 2)}`
          // 当前图标的颜色
          if (this.allowHalf && this.halfIcon) {
            style.color = this.activeIndex > index + 1 ? this.elActionColor : this.inactiveColor
          } else {
            style.color = this.activeIndex > index ? this.elActionColor : this.inactiveColor
          }
          return style
        }
      },
      // 半图标样式
      halfIconStyle() {
        let style = {}
        
        style.fontSize = this.$t.string.getLengthUnitValue(this.size)
        style.padding = `0 ${this.$t.string.getLengthUnitValue(this.gutter / 2)}`
        style.color = this.elActionColor
        return style
      }
    },
    data() {
      return {
        // 保证控件的唯一性
        elId: this.$t.uuid(),
        elClass: this.$t.uuid(),
        // 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
        starBoxLeft: 0,
        // 当前激活的星星的序号
        activeIndex: this.value,
        // 每个星星的宽度
        starWidth: 0,
        // 每个星星最右边到盒子组件最左边的距离
        starWidthArr: [],
        // 标记是否为半图标
        halfIcon: false,
      }
    },
    watch: {
      value(val) {
        this.activeIndex = val
        if (this.allowHalf && (val % 1 === 0.5)) {
          this.halfIcon = true
        } else {
          this.halfIcon = false
        }
      },
      size() {
        // 当尺寸修改的时候重新获取布局尺寸信息
        this.$nextTick(() => {
          this.getElRectById()
          this.getElRectByClass()
        })
      }
    },
    mounted() {
      this.getElRectById()
      this.getElRectByClass()
    },
    methods: {
      // 获取评分组件盒子的布局信息
      getElRectById() {
        this._tGetRect('#'+this.elId).then(res => {
          this.starBoxLeft = res.left
        })
      },
      // 获取单个星星的尺寸
      getElRectByClass() {
        this._tGetRect('.'+this.elClass).then(res => {
          this.starWidth = res.width
          // 把每个星星最右边到盒子最左边的距离
          for (let i = 0; i < this.count; i++) {
            this.starWidthArr[i] = (i + 1) * this.starWidth
          }
        })
      },
      // 手指滑动
      touchMove(e) {
        if (this.disabled) return
        if (!e.changedTouches[0]) return
        
        const movePageX = e.changedTouches[0].pageX
        // 滑动点相对于评分盒子左边的距离
        const distance = movePageX - this.starBoxLeft
        
        // 如果滑动到了评分盒子的左边界,设置为0星
        if (distance <= 0) {
          this.activeIndex = 0
        }
        
        // 计算滑动的距离相当于点击多少颗星星
        let index = Math.ceil(distance / this.starWidth)
        if (this.allowHalf) {
          const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
          if (distance < iconHalfWidth) {
            this.halfIcon = true
            index -= 0.5
          } else {
            this.halfIcon = false
          }
        }
        this.activeIndex = index > this.count ? this.count : index
        
        if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
        
        this.emitEvent()
      },
      // 通过点击直接选中
      click(index, e) {
        if (this.disabled) return
        // 半星选择
        if (this.allowHalf) {
          if (!e.changedTouches[0]) return
          const movePageX = e.changedTouches[0].pageX
          // 点击点相对于当前图标左边的距离
          const distance = movePageX - this.starBoxLeft
          const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
          if (distance < iconHalfWidth) {
            this.halfIcon = true
          } else {
            this.halfIcon = false
          }
        }
        
        // 对第一个星星特殊处理,只有一个的时候,点击可以取消,否则无法作0星评价
        if (index == 1) {
          if (this.allowHalf && this.allowHalf) {
            if ((this.activeIndex === 0.5 && this.halfIcon) || 
                (this.activeIndex === 1 && !this.halfIcon)) {
              this.activeIndex = 0
            } else {
              this.activeIndex = this.halfIcon ? 0.5 : 1
            }
          } else {
            if (this.activeIndex == 1) {
              this.activeIndex = 0
            } else {
              this.activeIndex = 1
            }
          }
        } else {
          this.activeIndex = (this.allowHalf && this.halfIcon) ? index - 0.5 : index
        }
        
        if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
        
        this.emitEvent()
      },
      // 发送事件
      emitEvent() {
        this.$emit('change', this.activeIndex)
        // 修改v-model的值
        this.$emit('input', this.activeIndex)
      }
    }
  }
</script>
 
<style lang="scss" scoped>
  
  .tn-rate {
    display: inline-flex;
    align-items: center;
    margin: 0;
    padding: 0;
    
    &__wrap {
      
      &__icon {
        position: relative;
        box-sizing: border-box;
        
        &--half {
          position: absolute;
          top: 0;
          left: 0;
          display: inline-block;
          overflow: hidden;
          width: 50%;
        }
      }
    }
  }
</style>