wd-rate.vue 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <template>
  2. <view :class="`wd-rate ${customClass}`" :style="customStyle" @touchmove="onTouchMove">
  3. <view
  4. v-for="(rate, index) in rateList"
  5. :key="index"
  6. :data-index="index"
  7. :style="{ 'margin-right': index == rateList.length - 1 ? 0 : space }"
  8. class="wd-rate__item"
  9. >
  10. <wd-icon
  11. custom-class="wd-rate__item-star"
  12. :name="isActive(rate) ? activeIcon : icon"
  13. :size="size"
  14. :custom-style="rate === '100%' ? iconActiveStyle : iconStyle"
  15. @click="handleClick(index, false)"
  16. />
  17. <view v-if="props.allowHalf" class="wd-rate__item-half" @click.stop="handleClick(index, true)">
  18. <wd-icon
  19. custom-class="wd-rate__item-star"
  20. :name="isActive(rate) ? activeIcon : icon"
  21. :size="size"
  22. :custom-style="rate !== '0' ? iconActiveStyle : iconStyle"
  23. />
  24. </view>
  25. </view>
  26. </view>
  27. </template>
  28. <script lang="ts">
  29. export default {
  30. name: 'wd-rate',
  31. options: {
  32. addGlobalClass: true,
  33. virtualHost: true,
  34. styleIsolation: 'shared'
  35. }
  36. }
  37. </script>
  38. <script lang="ts" setup>
  39. import wdIcon from '../wd-icon/wd-icon.vue'
  40. import { computed, getCurrentInstance, ref, watch } from 'vue'
  41. import { rateProps } from './types'
  42. import { getRect } from '../common/util'
  43. const { proxy } = getCurrentInstance() as any
  44. const props = defineProps(rateProps)
  45. const emit = defineEmits(['update:modelValue', 'change'])
  46. const rateList = ref<Array<string>>([])
  47. const activeValue = ref<string>('')
  48. const iconStyle = computed(() => {
  49. return `background:${props.color};`
  50. })
  51. const iconActiveStyle = computed(() => {
  52. return `background:${props.disabled ? props.disabledColor : activeValue.value};`
  53. })
  54. watch(
  55. () => props.activeColor,
  56. (newVal) => {
  57. if (Array.isArray(newVal) && !newVal.length) {
  58. console.error('activeColor cannot be an empty array')
  59. }
  60. computeActiveValue()
  61. },
  62. {
  63. immediate: true,
  64. deep: true
  65. }
  66. )
  67. watch(
  68. [() => props.num, () => props.modelValue],
  69. () => {
  70. computeRateList()
  71. },
  72. {
  73. immediate: true,
  74. deep: true
  75. }
  76. )
  77. // 当前选项是否为激活状态
  78. const isActive = (rate: string) => {
  79. return rate !== '0'
  80. }
  81. /**
  82. * @description 计算当前应当展示的rate数量
  83. */
  84. function computeRateList() {
  85. const { modelValue, num, allowHalf } = props
  86. // value和num都准备好才能计算
  87. if (modelValue === null || !num) return
  88. if (typeof modelValue !== 'number') {
  89. console.error('[wot ui] error(wd-rate): the value of wd-rate should be a number')
  90. return
  91. }
  92. const tempRateList: string[] = []
  93. const fullLength = Math.floor(modelValue)
  94. for (let i = 0; i < num; i++) {
  95. if (i < fullLength) {
  96. tempRateList.push('100%')
  97. } else if (i === fullLength && allowHalf && modelValue % 1 !== 0) {
  98. tempRateList.push('50%')
  99. } else {
  100. tempRateList.push('0')
  101. }
  102. }
  103. rateList.value = tempRateList
  104. computeActiveValue()
  105. }
  106. /**
  107. * @description 计算当前应当展示的rate颜色
  108. */
  109. function computeActiveValue() {
  110. const { activeColor, modelValue, num } = props
  111. let tempActiveValue: string = ''
  112. if (Array.isArray(activeColor) && activeColor.length) {
  113. tempActiveValue = Number(modelValue) <= num * 0.6 || !activeColor[1] ? activeColor[0] : activeColor[1]
  114. } else {
  115. tempActiveValue = activeColor as string
  116. }
  117. activeValue.value = tempActiveValue
  118. }
  119. /**
  120. * @description 处理点击事件
  121. * @param index 点击的索引
  122. * @param isHalf 是否为半星
  123. */
  124. function handleClick(index: number, isHalf: boolean) {
  125. const { readonly, disabled, clearable, allowHalf, modelValue } = props
  126. if (readonly || disabled) return
  127. let value = isHalf ? index + 0.5 : index + 1
  128. // 点击清空逻辑:当点击的值与当前modelValue相等且等于最小值时允许清空
  129. if (clearable) {
  130. const minValue = allowHalf ? 0.5 : 1
  131. if (value === modelValue && value === minValue) {
  132. value = 0
  133. }
  134. }
  135. updateValue(value)
  136. }
  137. /**
  138. * @description 设置评分值并触发事件
  139. */
  140. function updateValue(value: number) {
  141. emit('update:modelValue', value)
  142. emit('change', {
  143. value
  144. })
  145. }
  146. async function onTouchMove(event: TouchEvent) {
  147. const { clientX } = event.touches[0]
  148. const rateItems = await getRect('.wd-rate__item', true, proxy)
  149. const targetIndex = Array.from(rateItems).findIndex((rect) => {
  150. return clientX >= rect.left! && clientX <= rect.right!
  151. })
  152. if (targetIndex !== -1) {
  153. const target = rateItems[targetIndex]
  154. const itemWidth = target.width!
  155. const isHalf = props.allowHalf && clientX - target.left! < itemWidth / 2
  156. const value = isHalf ? targetIndex + 0.5 : targetIndex + 1
  157. if (value >= 0.5) {
  158. const value = isHalf ? targetIndex + 0.5 : targetIndex + 1
  159. updateValue(value)
  160. }
  161. }
  162. }
  163. </script>
  164. <style lang="scss" scoped>
  165. @import './index.scss';
  166. </style>