uv-rate.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. <template>
  2. <view
  3. class="uv-rate"
  4. :id="elId"
  5. ref="uv-rate"
  6. :style="[$uv.addStyle(customStyle)]"
  7. >
  8. <view class="uv-rate__content"
  9. @touchmove.stop="touchMove"
  10. @touchend.stop="touchEnd">
  11. <view class="uv-rate__content__item"
  12. v-for="(item, index) in Number(count)"
  13. :key="index"
  14. :class="[elClass]">
  15. <view class="uv-rate__content__item__icon-wrap"
  16. ref="uv-rate__content__item__icon-wrap"
  17. @tap.stop="clickHandler($event, index + 1)">
  18. <uv-icon
  19. :name="Math.floor(activeIndex) > index? activeIcon : inactiveIcon"
  20. :color="disabled ? '#c8c9cc' : Math.floor(activeIndex) > index ? activeColor : inactiveColor"
  21. :custom-style="{
  22. 'padding-left': $uv.addUnit(gutter / 2),
  23. 'padding-right': $uv.addUnit(gutter / 2)
  24. }"
  25. :size="size"
  26. ></uv-icon>
  27. </view>
  28. <view v-if="allowHalf"
  29. @tap.stop="clickHandler($event, index + 1)"
  30. class="uv-rate__content__item__icon-wrap uv-rate__content__item__icon-wrap--half"
  31. :style="[{ width: $uv.addUnit(rateWidth / 2)}]"
  32. ref="uv-rate__content__item__icon-wrap">
  33. <uv-icon
  34. :name=" Math.ceil(activeIndex) > index ? activeIcon : inactiveIcon "
  35. :color=" disabled ? '#c8c9cc' : Math.ceil(activeIndex) > index ? activeColor : inactiveColor "
  36. :custom-style="{
  37. 'padding-left': $uv.addUnit(gutter / 2),
  38. 'padding-right': $uv.addUnit(gutter / 2)
  39. }"
  40. :size="size">
  41. </uv-icon>
  42. </view>
  43. </view>
  44. </view>
  45. </view>
  46. </template>
  47. <script>
  48. import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
  49. import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
  50. import props from './props.js';
  51. // #ifdef APP-NVUE
  52. const dom = weex.requireModule("dom");
  53. // #endif
  54. /**
  55. * rate 评分
  56. * @description 该组件一般用于满意度调查,星型评分的场景
  57. * @tutorial https://www.uvui.cn/components/rate.html
  58. * @property {String | Number} value 用于v-model双向绑定选中的星星数量 (默认 1 )
  59. * @property {String | Number} count 最多可选的星星数量 (默认 5 )
  60. * @property {Boolean} disabled 是否禁止用户操作 (默认 false )
  61. * @property {Boolean} readonly 是否只读 (默认 false )
  62. * @property {String | Number} size 星星的大小,单位px (默认 18 )
  63. * @property {String} inactiveColor 未选中星星的颜色 (默认 '#b2b2b2' )
  64. * @property {String} activeColor 选中的星星颜色 (默认 '#FA3534' )
  65. * @property {String | Number} gutter 星星之间的距离 (默认 4 )
  66. * @property {String | Number} minCount 最少选中星星的个数 (默认 1 )
  67. * @property {Boolean} allowHalf 是否允许半星选择 (默认 false )
  68. * @property {String} activeIcon 选中时的图标名,只能为uvui的内置图标 (默认 'star-fill' )
  69. * @property {String} inactiveIcon 未选中时的图标名,只能为uvui的内置图标 (默认 'star' )
  70. * @property {Boolean} touchable 是否可以通过滑动手势选择评分 (默认 'false' )
  71. * @property {Object} customStyle 组件的样式,对象形式
  72. * @event {Function} change 选中的星星发生变化时触发
  73. * @example <uv-rate :count="count" :value="2"></uv-rate>
  74. */
  75. export default {
  76. name: "uv-rate",
  77. mixins: [mpMixin, mixin, props],
  78. data() {
  79. return {
  80. // 生成一个唯一id,否则一个页面多个评分组件,会造成冲突
  81. elId: '',
  82. elClass: '',
  83. rateBoxLeft: 0, // 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
  84. activeIndex: 0,
  85. rateWidth: 0, // 每个星星的宽度
  86. // 标识是否正在滑动,由于iOS事件上touch比click先触发,导致快速滑动结束后,接着触发click,导致事件混乱而出错
  87. moving: false
  88. }
  89. },
  90. watch: {
  91. value(newVal){
  92. this.activeIndex = newVal;
  93. },
  94. modelValue(newVal){
  95. this.activeIndex = newVal;
  96. }
  97. },
  98. created() {
  99. this.activeIndex = Number(this.value || this.modelValue);
  100. this.elId = this.$uv.guid();
  101. this.elClass = this.$uv.guid();
  102. },
  103. mounted() {
  104. this.init();
  105. },
  106. methods: {
  107. init() {
  108. this.$uv.sleep(200).then(() => {
  109. this.getRateItemRect();
  110. this.getRateIconWrapRect();
  111. })
  112. },
  113. // 获取评分组件盒子的布局信息
  114. async getRateItemRect() {
  115. await this.$uv.sleep();
  116. // uvui封装的获取节点的方法,详见文档
  117. // #ifndef APP-NVUE
  118. this.$uvGetRect("#" + this.elId).then((res) => {
  119. this.rateBoxLeft = res.left;
  120. });
  121. // #endif
  122. // #ifdef APP-NVUE
  123. dom.getComponentRect(this.$refs["uv-rate"], (res) => {
  124. this.rateBoxLeft = res.size.left;
  125. });
  126. // #endif
  127. },
  128. // 获取单个星星的尺寸
  129. getRateIconWrapRect() {
  130. // uvui封装的获取节点的方法,详见文档
  131. // #ifndef APP-NVUE
  132. this.$uvGetRect("." + this.elClass).then((res) => {
  133. this.rateWidth = res.width;
  134. });
  135. // #endif
  136. // #ifdef APP-NVUE
  137. dom.getComponentRect(this.$refs["uv-rate__content__item__icon-wrap"][0],
  138. (res) => {
  139. this.rateWidth = res.size.width;
  140. });
  141. // #endif
  142. },
  143. // 手指滑动
  144. touchMove(e) {
  145. // 如果禁止通过手动滑动选择,返回
  146. if (!this.touchable) {
  147. return;
  148. }
  149. this.preventEvent(e);
  150. const x = e.changedTouches && e.changedTouches[0].pageX || e.detail && e.detail.pageX;
  151. this.getActiveIndex(x);
  152. },
  153. // 停止滑动
  154. touchEnd(e) {
  155. // 如果禁止通过手动滑动选择,返回
  156. if (!this.touchable) {
  157. return;
  158. }
  159. this.preventEvent(e);
  160. const x = e.changedTouches && e.changedTouches[0].pageX || e.detail && e.detail.pageX;
  161. this.getActiveIndex(x);
  162. },
  163. // 通过点击,直接选中
  164. clickHandler(e, index) {
  165. // ios上,moving状态取消事件触发
  166. if (this.$uv.os() === "ios" && this.moving) {
  167. return;
  168. }
  169. this.preventEvent(e);
  170. let x = 0;
  171. // 点击时,在nvue上,无法获得点击的坐标,所以无法实现点击半星选择
  172. // #ifndef APP-NVUE
  173. x = e.changedTouches && e.changedTouches[0].pageX || e.detail && e.detail.pageX;
  174. // #endif
  175. // #ifdef APP-NVUE
  176. // nvue下,无法通过点击获得坐标信息,这里通过元素的位置尺寸值模拟坐标
  177. x = index * this.rateWidth + this.rateBoxLeft;
  178. // #endif
  179. this.getActiveIndex(x, true);
  180. },
  181. // 发出事件
  182. changeEvent() {
  183. this.$emit("change", this.activeIndex);
  184. this.$emit("input", this.activeIndex);
  185. this.$emit("update:modelValue", this.activeIndex);
  186. },
  187. // 获取当前激活的评分图标
  188. getActiveIndex(x, isClick = false) {
  189. if (this.disabled || this.readonly) {
  190. return;
  191. }
  192. // 判断当前操作的点的x坐标值,是否在允许的边界范围内
  193. const allRateWidth = this.rateWidth * this.count + this.rateBoxLeft;
  194. // 如果小于第一个图标的左边界,设置为最小值,如果大于所有图标的宽度,则设置为最大值
  195. x = this.$uv.range(this.rateBoxLeft, allRateWidth, x) - this.rateBoxLeft
  196. // 滑动点相对于评分盒子左边的距离
  197. const distance = x;
  198. // 滑动的距离,相当于多少颗星星
  199. let index;
  200. // 判断是否允许半星
  201. if (this.allowHalf) {
  202. index = Math.floor(distance / this.rateWidth);
  203. // 取余,判断小数的区间范围
  204. const decimal = distance % this.rateWidth;
  205. if (decimal <= this.rateWidth / 2 && decimal > 0) {
  206. index += 0.5;
  207. } else if (decimal > this.rateWidth / 2) {
  208. index++;
  209. }
  210. } else {
  211. index = Math.floor(distance / this.rateWidth);
  212. // 取余,判断小数的区间范围
  213. const decimal = distance % this.rateWidth;
  214. // 非半星时,只有超过了图标的一半距离,才认为是选择了这颗星
  215. if (isClick) {
  216. if (decimal > 0) index++;
  217. } else {
  218. if (decimal > this.rateWidth / 2) index++;
  219. }
  220. }
  221. this.activeIndex = Math.min(index, this.count);
  222. // 对最少颗星星的限制
  223. if (this.activeIndex < this.minCount) {
  224. this.activeIndex = this.minCount;
  225. }
  226. this.changeEvent();
  227. // 设置延时为了让click事件在touchmove之前触发
  228. setTimeout(() => {
  229. this.moving = true;
  230. }, 10);
  231. // 一定时间后,取消标识为移动中状态,是为了让click事件无效
  232. setTimeout(() => {
  233. this.moving = false;
  234. }, 10);
  235. }
  236. }
  237. }
  238. </script>
  239. <style lang="scss" scoped>
  240. @import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
  241. @import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
  242. $uv-rate-margin: 0 !default;
  243. $uv-rate-padding: 0 !default;
  244. $uv-rate-item-icon-wrap-half-top: 0 !default;
  245. $uv-rate-item-icon-wrap-half-left: 0 !default;
  246. .uv-rate {
  247. @include flex;
  248. align-items: center;
  249. margin: $uv-rate-margin;
  250. padding: $uv-rate-padding;
  251. /* #ifndef APP-NVUE */
  252. touch-action: none;
  253. /* #endif */
  254. &__content {
  255. @include flex;
  256. &__item {
  257. position: relative;
  258. &__icon-wrap {
  259. &--half {
  260. position: absolute;
  261. overflow: hidden;
  262. top: $uv-rate-item-icon-wrap-half-top;
  263. left: $uv-rate-item-icon-wrap-half-left;
  264. }
  265. }
  266. }
  267. }
  268. }
  269. .uv-icon {
  270. /* #ifndef APP-NVUE */
  271. box-sizing: border-box;
  272. /* #endif */
  273. }
  274. </style>