cl-rate.uvue 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. <template>
  2. <view class="cl-rate" :class="[{ 'cl-rate--disabled': isDisabled }, pt.className]">
  3. <view
  4. v-for="(item, index) in max"
  5. :key="index"
  6. class="cl-rate__item"
  7. :class="[
  8. {
  9. 'cl-rate__item--active': item <= modelValue
  10. },
  11. pt.item?.className
  12. ]"
  13. @touchstart="onTap(index)"
  14. >
  15. <cl-icon
  16. :name="voidIcon"
  17. :color="voidColor"
  18. :size="size"
  19. :pt="{
  20. className: `${pt.icon?.className}`
  21. }"
  22. ></cl-icon>
  23. <cl-icon
  24. v-if="getIconActiveWidth(item) > 0"
  25. :name="icon"
  26. :color="color"
  27. :size="size"
  28. :width="getIconActiveWidth(item)"
  29. :pt="{
  30. className: `absolute left-0 ${pt.icon?.className}`
  31. }"
  32. ></cl-icon>
  33. </view>
  34. <cl-text
  35. v-if="showScore"
  36. :pt="{
  37. className: parseClass(['cl-rate__score ml-2 font-bold', pt.score?.className])
  38. }"
  39. >{{ modelValue }}</cl-text
  40. >
  41. </view>
  42. </template>
  43. <script setup lang="ts">
  44. import { computed } from "vue";
  45. import { parseClass, parsePt } from "@/cool";
  46. import type { PassThroughProps } from "../../types";
  47. import type { ClIconProps } from "../cl-icon/props";
  48. import { useForm } from "../../hooks";
  49. defineOptions({
  50. name: "cl-rate"
  51. });
  52. // 组件属性定义
  53. const props = defineProps({
  54. // 样式穿透
  55. pt: {
  56. type: Object,
  57. default: () => ({})
  58. },
  59. // 评分值
  60. modelValue: {
  61. type: Number,
  62. default: 0
  63. },
  64. // 最大评分
  65. max: {
  66. type: Number,
  67. default: 5
  68. },
  69. // 是否禁用
  70. disabled: {
  71. type: Boolean,
  72. default: false
  73. },
  74. // 是否允许半星
  75. allowHalf: {
  76. type: Boolean,
  77. default: false
  78. },
  79. // 是否显示分数
  80. showScore: {
  81. type: Boolean,
  82. default: false
  83. },
  84. // 组件尺寸
  85. size: {
  86. type: Number,
  87. default: 40
  88. },
  89. // 图标名称
  90. icon: {
  91. type: String,
  92. default: "star-fill"
  93. },
  94. // 未激活图标
  95. voidIcon: {
  96. type: String,
  97. default: "star-fill"
  98. },
  99. // 激活颜色
  100. color: {
  101. type: String,
  102. default: "primary"
  103. },
  104. // 默认颜色
  105. voidColor: {
  106. type: String,
  107. default: "#dddddd"
  108. }
  109. });
  110. // 定义事件
  111. const emit = defineEmits(["update:modelValue", "change"]);
  112. // 透传样式类型定义
  113. type PassThrough = {
  114. className?: string;
  115. item?: PassThroughProps;
  116. icon?: ClIconProps;
  117. score?: PassThroughProps;
  118. };
  119. // 解析透传样式
  120. const pt = computed(() => parsePt<PassThrough>(props.pt));
  121. // cl-form 上下文
  122. const { disabled } = useForm();
  123. // 是否禁用
  124. const isDisabled = computed(() => props.disabled || disabled.value);
  125. // 获取图标激活宽度
  126. function getIconActiveWidth(item: number) {
  127. // 如果评分值大于等于当前项,返回null表示完全填充
  128. if (props.modelValue >= item) {
  129. return props.size;
  130. }
  131. // 如果评分值的整数部分小于当前项,返回0表示不填充
  132. if (Math.floor(props.modelValue) < item - 1) {
  133. return 0;
  134. }
  135. // 处理小数部分填充
  136. return Math.floor((props.modelValue % 1) * props.size);
  137. }
  138. // 点击事件处理
  139. function onTap(index: number) {
  140. if (isDisabled.value) {
  141. return;
  142. }
  143. let value: number;
  144. if (props.allowHalf) {
  145. // 半星逻辑:点击同一位置切换半星和整星
  146. const currentValue = index + 1;
  147. if (props.modelValue == currentValue) {
  148. value = index + 0.5;
  149. } else if (props.modelValue == index + 0.5) {
  150. value = index;
  151. } else {
  152. value = currentValue;
  153. }
  154. } else {
  155. value = index + 1;
  156. }
  157. // 确保值在有效范围内
  158. value = Math.max(0, Math.min(value, props.max));
  159. emit("update:modelValue", value);
  160. emit("change", value);
  161. }
  162. </script>
  163. <style lang="scss" scoped>
  164. .cl-rate {
  165. @apply flex flex-row items-center;
  166. &--disabled {
  167. @apply opacity-50;
  168. }
  169. &__item {
  170. @apply flex items-center justify-center relative duration-200 overflow-hidden;
  171. transition-property: color;
  172. margin-right: 6rpx;
  173. }
  174. }
  175. </style>