Rate.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <template>
  2. <view
  3. :id="id"
  4. class="nana-rate"
  5. @touchstart="handleTouchStart"
  6. @touchmove="handleTouchMove"
  7. @touchend="handleTouchEnd"
  8. @mouseup="handleTouchEnd"
  9. @mousedown="handleTouchStart"
  10. @mousemove="handleTouchMove"
  11. >
  12. <template
  13. v-for="i of count"
  14. :key="i"
  15. >
  16. <view v-if="i - 1 === Math.floor(value) && i - 1 < Math.ceil(value)" class="active-half">
  17. <view :style="(starActiveHalfStyle as any)">
  18. <Icon
  19. :icon="icon"
  20. v-bind="props.starActiveStyle"
  21. :color="disabled ? starDisableColor : starActiveColor"
  22. :size="size"
  23. :style="starActiveStyle"
  24. />
  25. </view>
  26. <Icon
  27. :icon="voidIcon"
  28. v-bind="props.starStyle"
  29. :color="disabled ? starDisableColor : starColor"
  30. :size="size"
  31. :style="starDeactiveStyle"
  32. />
  33. </view>
  34. <Icon
  35. v-else-if="i <= value"
  36. :icon="icon"
  37. v-bind="props.starActiveStyle"
  38. :color="disabled ? starDisableColor : starActiveColor"
  39. :size="size"
  40. :style="starActiveStyle"
  41. />
  42. <Icon
  43. v-else-if="i > value"
  44. :icon="voidIcon"
  45. v-bind="props.starStyle"
  46. :color="disabled ? starDisableColor : starColor"
  47. :size="size"
  48. :style="starDeactiveStyle"
  49. />
  50. </template>
  51. </view>
  52. </template>
  53. <script setup lang="ts">
  54. import { computed, getCurrentInstance, toRef } from 'vue';
  55. import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
  56. import { useFieldChildValueInjector } from './FormContext';
  57. import type { IconProps } from '../basic/Icon.vue';
  58. import Icon from '../basic/Icon.vue';
  59. import { RandomUtils } from '@imengyu/imengyu-utils';
  60. import { rpx2px } from '../theme/ThemeTools';
  61. export interface RateProps {
  62. /**
  63. * 评星组件参数值
  64. * @default 0
  65. */
  66. modelValue?: number;
  67. /**
  68. * 星星的数量
  69. * @default 5
  70. */
  71. count?: number;
  72. /**
  73. * 是否可以为0星
  74. * @default false
  75. */
  76. canbeZero?: boolean,
  77. /**
  78. * 星星未激活自定义样式
  79. */
  80. starStyle?: IconProps;
  81. /**
  82. * 星星激活自定义样式
  83. */
  84. starActiveStyle?: IconProps;
  85. /**
  86. * 星星激活的颜色
  87. * @default warning
  88. */
  89. starActiveColor?: string;
  90. /**
  91. * 星星未激活的颜色
  92. * @default grey
  93. */
  94. starColor?: string;
  95. /**
  96. * 星星禁用的颜色
  97. * @default grey
  98. */
  99. starDisableColor?: string;
  100. /**
  101. * 选中时的图标
  102. * @default 'favorite-filling'
  103. */
  104. icon?: string;
  105. /**
  106. * 未选中时的图标
  107. * @default 'favorite'
  108. */
  109. voidIcon?: string;
  110. /**
  111. * 星星之间的间距
  112. * @default 3
  113. */
  114. space?: number;
  115. /**
  116. * 用户是否可以选择出半星
  117. * @default false
  118. */
  119. half?: boolean;
  120. /**
  121. * 是否只读
  122. * @default false
  123. */
  124. readonly?: boolean;
  125. /**
  126. * 评星组件是否禁用
  127. * @default false
  128. */
  129. disabled?: boolean;
  130. /**
  131. * 评星组件大小
  132. * @default 48
  133. */
  134. size?: number;
  135. }
  136. const emit = defineEmits([ 'update:modelValue' ])
  137. const props = withDefaults(defineProps<RateProps>(), {
  138. modelValue: 0,
  139. count: 5,
  140. starActiveColor: () => propGetThemeVar('RateStarActiveColor', 'warning'),
  141. starColor: () => propGetThemeVar('RateStarColor', 'grey'),
  142. starDisableColor: () => propGetThemeVar('RateStarDisableColor', 'grey'),
  143. icon: () => propGetThemeVar('RateIcon', 'favorite-filling'),
  144. voidIcon: () => propGetThemeVar('RateVoidIcon', 'favorite'),
  145. space: () => propGetThemeVar('RateSpace', 3),
  146. size: () => propGetThemeVar('RateSize', 48),
  147. });
  148. const themeContext = useTheme();
  149. const starActiveStyle = computed(() => ({
  150. marginRight: themeContext.resolveThemeSize(props.space),
  151. ...props.starActiveStyle,
  152. }));
  153. const starActiveHalfStyle = computed(() => ({
  154. ...props.starActiveStyle,
  155. position: 'absolute',
  156. left: 0,
  157. top: 0,
  158. width: '50%',
  159. overflow: 'hidden',
  160. zIndex: 1,
  161. }));
  162. const starDeactiveStyle = computed(() => ({
  163. marginRight: themeContext.resolveThemeSize(props.space),
  164. ...props.starStyle,
  165. }));
  166. const id = RandomUtils.genNonDuplicateID(12);
  167. const instance = getCurrentInstance();
  168. const width = computed(() => (props.size + props.space) * props.count);
  169. const {
  170. value,
  171. updateValue,
  172. } = useFieldChildValueInjector(
  173. toRef(props, 'modelValue'),
  174. (v) => emit('update:modelValue', v)
  175. );
  176. let absLeft = 0;
  177. let pressed = false;
  178. function handleDrag(x: number) {
  179. let v = (x - absLeft) / rpx2px(width.value) * props.count;
  180. if (props.half) {
  181. let demcial = v - Math.floor(v);
  182. if (demcial < 0.2) demcial = 0;
  183. else if (demcial > 0.8) demcial = 1;
  184. else demcial = 0.5;
  185. v = Math.floor(v) + demcial;
  186. }
  187. else {
  188. v = Math.ceil(v);
  189. }
  190. if (v === 0 && !props.canbeZero)
  191. v = props.half ? 0.5 : 1;
  192. v = Math.max(0, Math.min(v, props.count));
  193. if (v !== value.value) {
  194. updateValue(v);
  195. }
  196. }
  197. function handleTouchStart(e: any) {
  198. if (props.readonly || props.disabled)
  199. return;
  200. e.stopPropagation();
  201. const query = uni.createSelectorQuery();
  202. query
  203. .in(instance)
  204. .select('#' + id)
  205. .boundingClientRect((res) => {
  206. if (res)
  207. absLeft = (res as any).left;
  208. handleDrag(e.touches[0]?.clientX);
  209. pressed = true;
  210. }).exec();
  211. }
  212. function handleTouchMove(e: any) {
  213. if (props.readonly || props.disabled)
  214. return;
  215. if (!pressed)
  216. return;
  217. e.stopPropagation();
  218. handleDrag(e.touches[0]?.clientX);
  219. }
  220. function handleTouchEnd() {
  221. pressed = false;
  222. }
  223. defineOptions({
  224. options: {
  225. styleIsolation: "shared",
  226. virtualHost: true,
  227. }
  228. })
  229. </script>
  230. <style lang="scss">
  231. .nana-rate {
  232. display: flex;
  233. flex-direction: row;
  234. position: relative;
  235. overflow: hidden;
  236. .active-half {
  237. position: relative;
  238. }
  239. }
  240. </style>