You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

328 lines
10 KiB

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