StarRating.vue 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. <script setup lang="ts">
  2. import StarSvg from '~/assets/icons/star.svg'
  3. import StarActiveSvg from '~/assets/icons/star-active.svg'
  4. interface Props {
  5. // Current rating value (1-5)
  6. modelValue?: number
  7. // Display mode: 'display' for readonly, 'rate' for interactive
  8. mode?: 'display' | 'rate'
  9. // Size of the star icon
  10. size?: number
  11. // Total number of stars
  12. maxStars?: number
  13. }
  14. interface Emits {
  15. (e: 'update:modelValue' | 'change', value: number): void
  16. }
  17. const props = withDefaults(defineProps<Props>(), {
  18. modelValue: 0,
  19. mode: 'display',
  20. size: 40,
  21. maxStars: 5,
  22. })
  23. const emit = defineEmits<Emits>()
  24. const hoveredStar = ref<number | null>(null)
  25. const handleStarClick = (star: number) => {
  26. if (props.mode === 'rate') {
  27. emit('update:modelValue', star)
  28. emit('change', star)
  29. }
  30. }
  31. const handleStarHover = (star: number) => {
  32. if (props.mode === 'rate') {
  33. hoveredStar.value = star
  34. }
  35. }
  36. const handleStarLeave = () => {
  37. if (props.mode === 'rate') {
  38. hoveredStar.value = null
  39. }
  40. }
  41. const isStarActive = (star: number) => {
  42. if (props.mode === 'rate' && hoveredStar.value !== null) {
  43. return star <= hoveredStar.value
  44. }
  45. return star <= props.modelValue
  46. }
  47. </script>
  48. <template>
  49. <div
  50. class="star-rating flex items-center gap-1"
  51. :class="{ 'star-rating--interactive': mode === 'rate' }"
  52. >
  53. <button
  54. v-for="star in maxStars"
  55. :key="`star-${star}`"
  56. type="button"
  57. class="star-button"
  58. :class="{
  59. 'star-button--interactive': mode === 'rate',
  60. 'star-button--active': isStarActive(star),
  61. }"
  62. :disabled="mode === 'display'"
  63. @click="handleStarClick(star)"
  64. @mouseenter="handleStarHover(star)"
  65. @mouseleave="handleStarLeave"
  66. >
  67. <component
  68. :is="isStarActive(star) ? StarActiveSvg : StarSvg"
  69. :style="{ width: `${size}px`, height: `${size}px` }"
  70. />
  71. </button>
  72. </div>
  73. </template>
  74. <style lang="scss" scoped>
  75. .star-rating {
  76. display: flex;
  77. align-items: center;
  78. .star-button {
  79. padding: 0;
  80. border: none;
  81. background: none;
  82. cursor: default;
  83. transition: transform 0.15s ease;
  84. display: flex;
  85. align-items: center;
  86. justify-content: center;
  87. &--interactive {
  88. cursor: pointer;
  89. &:hover {
  90. transform: scale(1.1);
  91. }
  92. &:active {
  93. transform: scale(0.95);
  94. }
  95. }
  96. &:disabled {
  97. cursor: default;
  98. }
  99. }
  100. }
  101. </style>