OrderRating.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <script setup lang="ts">
  2. import { useI18n } from 'vue-i18n'
  3. import StarRating from '~/components/common/StarRating.vue'
  4. interface Props {
  5. show: boolean
  6. /**
  7. * 当前评分 1-5
  8. */
  9. modelValue?: number
  10. /**
  11. * 头像地址(可选)
  12. */
  13. avatar?: string
  14. }
  15. const props = withDefaults(defineProps<Props>(), {
  16. modelValue: 0,
  17. avatar: '',
  18. })
  19. const emit = defineEmits<{
  20. 'update:show': [value: boolean]
  21. 'update:modelValue': [value: number]
  22. 'submit': [value: number]
  23. }>()
  24. const { t } = useI18n()
  25. const img = useImage()
  26. const localScore = ref(props.modelValue || 0)
  27. watch(
  28. () => props.modelValue,
  29. (val) => {
  30. if (val !== undefined) {
  31. localScore.value = val
  32. }
  33. },
  34. )
  35. const handleUpdateShow = (value: boolean) => {
  36. emit('update:show', value)
  37. }
  38. const handleChange = (value: number) => {
  39. localScore.value = value
  40. emit('update:modelValue', value)
  41. }
  42. const satisfactionKey = computed(() => {
  43. if (!localScore.value)
  44. return 'order.rating.level.none'
  45. if (localScore.value <= 1)
  46. return 'order.rating.level.1'
  47. if (localScore.value === 2)
  48. return 'order.rating.level.2'
  49. if (localScore.value === 3)
  50. return 'order.rating.level.3'
  51. if (localScore.value === 4)
  52. return 'order.rating.level.4'
  53. return 'order.rating.level.5'
  54. })
  55. const satisfactionLabel = computed(() => t(satisfactionKey.value))
  56. const handleSubmit = () => {
  57. if (!localScore.value)
  58. return
  59. emit('submit', localScore.value)
  60. }
  61. </script>
  62. <template>
  63. <van-popup
  64. :show="props.show"
  65. round
  66. position="bottom"
  67. class="order-rating-popup"
  68. @update:show="handleUpdateShow"
  69. >
  70. <div class="order-rating-popup__container">
  71. <div class="order-rating-popup__avatar-wrapper">
  72. <div class="order-rating-popup__avatar">
  73. <NuxtImg
  74. :src="avatar"
  75. :alt="avatar"
  76. loading="lazy"
  77. :placeholder="img('/avatar.png')"
  78. />
  79. </div>
  80. </div>
  81. <p class="order-rating-popup__title">
  82. {{ t('order.rating.title') }}
  83. </p>
  84. <div class="order-rating-popup__satisfaction-row">
  85. <span class="order-rating-popup__satisfaction-label">
  86. {{ t('order.rating.satisfactionLabel') }}
  87. </span>
  88. <div class="order-rating-popup__stars">
  89. <StarRating
  90. v-model="localScore"
  91. mode="rate"
  92. :size="30"
  93. :max-stars="5"
  94. @change="handleChange"
  95. />
  96. </div>
  97. <span class="order-rating-popup__satisfaction-text">
  98. {{ satisfactionLabel }}
  99. </span>
  100. </div>
  101. <button
  102. class="order-rating-popup__submit"
  103. type="button"
  104. :disabled="!localScore"
  105. @click="handleSubmit"
  106. >
  107. {{ t('order.rating.submit') }}
  108. </button>
  109. </div>
  110. </van-popup>
  111. </template>
  112. <style scoped lang="scss">
  113. .order-rating-popup {
  114. background-color: transparent;
  115. &__container {
  116. position: relative;
  117. padding: 40px 16px 24px;
  118. border-radius: 15px 15px 0 0;
  119. background-color: #fff;
  120. display: flex;
  121. flex-direction: column;
  122. align-items: center;
  123. gap: 20px;
  124. }
  125. &__avatar-wrapper {
  126. position: absolute;
  127. top: -30px;
  128. left: 50%;
  129. transform: translateX(-50%);
  130. width: 60px;
  131. height: 60px;
  132. border-radius: 50%;
  133. overflow: hidden;
  134. background: #fff;
  135. display: flex;
  136. align-items: center;
  137. justify-content: center;
  138. }
  139. &__avatar {
  140. width: 96%;
  141. height: 96%;
  142. border-radius: 50%;
  143. overflow: hidden;
  144. img {
  145. width: 100%;
  146. height: 100%;
  147. object-fit: cover;
  148. display: block;
  149. }
  150. }
  151. &__title {
  152. margin-top: 10px;
  153. font-family: var(--font-title);
  154. font-size: 16px;
  155. font-weight: 600;
  156. color: #1d2129;
  157. text-align: center;
  158. }
  159. &__satisfaction-row {
  160. width: 100%;
  161. display: flex;
  162. align-items: center;
  163. justify-content: center;
  164. position: relative;
  165. }
  166. &__satisfaction-label,
  167. &__satisfaction-text {
  168. font-size: 12px;
  169. line-height: 16px;
  170. color: #1d2129;
  171. white-space: nowrap;
  172. position: absolute;
  173. z-index: 1;
  174. }
  175. &__satisfaction-label {
  176. left: 0;
  177. text-align: left;
  178. }
  179. &__satisfaction-text {
  180. right: 0;
  181. text-align: right;
  182. }
  183. &__stars {
  184. width: 200px;
  185. display: flex;
  186. justify-content: center;
  187. align-items: center;
  188. position: relative;
  189. z-index: 2;
  190. }
  191. &__submit {
  192. width: 100%;
  193. height: 47px;
  194. border-radius: 100px;
  195. border: none;
  196. background: linear-gradient(90deg, #2f95ff 28.37%, #50ffd8 100%);
  197. font-family: var(--font-title);
  198. font-size: 16px;
  199. font-weight: 600;
  200. color: #fff;
  201. cursor: pointer;
  202. }
  203. &__submit:disabled {
  204. opacity: 0.5;
  205. cursor: default;
  206. }
  207. }
  208. </style>
  209. <style lang="scss">
  210. .van-popup.order-rating-popup {
  211. overflow-y: unset;
  212. }
  213. </style>