307 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | ||
|     <view
 | ||
|         class="u-rate"
 | ||
|         :id="elId"
 | ||
|         ref="u-rate"
 | ||
|         :style="[$u.addStyle(customStyle)]"
 | ||
|     >
 | ||
|         <view
 | ||
|             class="u-rate__content"
 | ||
|             @touchmove.stop="touchMove"
 | ||
|             @touchend.stop="touchEnd"
 | ||
|         >
 | ||
|             <view
 | ||
|                 class="u-rate__content__item"
 | ||
|                 v-for="(item, index) in Number(count)"
 | ||
|                 :key="index"
 | ||
|                 :class="[elClass]"
 | ||
|             >
 | ||
|                 <view
 | ||
|                     class="u-rate__content__item__icon-wrap"
 | ||
|                     ref="u-rate__content__item__icon-wrap"
 | ||
|                     @tap.stop="clickHandler($event, index + 1)"
 | ||
|                 >
 | ||
|                     <u-icon
 | ||
|                         :name="
 | ||
|                             Math.floor(activeIndex) > index
 | ||
|                                 ? activeIcon
 | ||
|                                 : inactiveIcon
 | ||
|                         "
 | ||
|                         :color="
 | ||
|                             disabled
 | ||
|                                 ? '#c8c9cc'
 | ||
|                                 : Math.floor(activeIndex) > index
 | ||
|                                 ? activeColor
 | ||
|                                 : inactiveColor
 | ||
|                         "
 | ||
|                         :custom-style="{
 | ||
|                             'padding-left': $u.addUnit(gutter / 2),
 | ||
| 							'padding-right': $u.addUnit(gutter / 2)
 | ||
|                         }"
 | ||
|                         :size="size"
 | ||
|                     ></u-icon>
 | ||
|                 </view>
 | ||
|                 <view
 | ||
|                     v-if="allowHalf"
 | ||
|                     @tap.stop="clickHandler($event, index + 1)"
 | ||
|                     class="u-rate__content__item__icon-wrap u-rate__content__item__icon-wrap--half"
 | ||
|                     :style="[{
 | ||
|                         width: $u.addUnit(rateWidth / 2),
 | ||
|                     }]"
 | ||
|                     ref="u-rate__content__item__icon-wrap"
 | ||
|                 >
 | ||
|                     <u-icon
 | ||
|                         :name="
 | ||
|                             Math.ceil(activeIndex) > index
 | ||
|                                 ? activeIcon
 | ||
|                                 : inactiveIcon
 | ||
|                         "
 | ||
|                         :color="
 | ||
|                             disabled
 | ||
|                                 ? '#c8c9cc'
 | ||
|                                 : Math.ceil(activeIndex) > index
 | ||
|                                 ? activeColor
 | ||
|                                 : inactiveColor
 | ||
|                         "
 | ||
|                         :custom-style="{
 | ||
| 							'padding-left': $u.addUnit(gutter / 2),
 | ||
| 							'padding-right': $u.addUnit(gutter / 2)
 | ||
|                         }"
 | ||
|                         :size="size"
 | ||
|                     ></u-icon>
 | ||
|                 </view>
 | ||
|             </view>
 | ||
|         </view>
 | ||
|     </view>
 | ||
| </template>
 | ||
| 
 | ||
| <script>
 | ||
| 	import props from './props.js';
 | ||
| 
 | ||
| 	// #ifdef APP-NVUE
 | ||
| 	const dom = weex.requireModule("dom");
 | ||
| 	// #endif
 | ||
| 	/**
 | ||
| 	 * rate 评分
 | ||
| 	 * @description 该组件一般用于满意度调查,星型评分的场景
 | ||
| 	 * @tutorial https://www.uviewui.com/components/rate.html
 | ||
| 	 * @property {String | Number}	value			用于v-model双向绑定选中的星星数量 (默认 1 )
 | ||
| 	 * @property {String | Number}	count			最多可选的星星数量 (默认 5 )
 | ||
| 	 * @property {Boolean}			disabled		是否禁止用户操作 (默认 false )
 | ||
| 	 * @property {Boolean}			readonly		是否只读 (默认 false )
 | ||
| 	 * @property {String | Number}	size			星星的大小,单位px (默认 18 )
 | ||
| 	 * @property {String}			inactiveColor	未选中星星的颜色 (默认 '#b2b2b2' )
 | ||
| 	 * @property {String}			activeColor		选中的星星颜色 (默认 '#FA3534' )
 | ||
| 	 * @property {String | Number}	gutter			星星之间的距离 (默认 4 )
 | ||
| 	 * @property {String | Number}	minCount		最少选中星星的个数 (默认 1 )
 | ||
| 	 * @property {Boolean}			allowHalf		是否允许半星选择 (默认 false )
 | ||
| 	 * @property {String}			activeIcon		选中时的图标名,只能为uView的内置图标 (默认 'star-fill' )
 | ||
| 	 * @property {String}			inactiveIcon	未选中时的图标名,只能为uView的内置图标 (默认 'star' )
 | ||
| 	 * @property {Boolean}			touchable		是否可以通过滑动手势选择评分 (默认 'true' )
 | ||
| 	 * @property {Object}			customStyle		组件的样式,对象形式
 | ||
| 	 * @event {Function} change 选中的星星发生变化时触发
 | ||
| 	 * @example <u-rate :count="count" :value="2"></u-rate>
 | ||
| 	 */
 | ||
| 	export default {
 | ||
| 		name: "u-rate",
 | ||
| 		mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
 | ||
| 		data() {
 | ||
| 			return {
 | ||
| 				// 生成一个唯一id,否则一个页面多个评分组件,会造成冲突
 | ||
| 				elId: uni.$u.guid(),
 | ||
| 				elClass: uni.$u.guid(),
 | ||
| 				rateBoxLeft: 0, // 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
 | ||
| 				activeIndex: this.value,
 | ||
| 				rateWidth: 0, // 每个星星的宽度
 | ||
| 				// 标识是否正在滑动,由于iOS事件上touch比click先触发,导致快速滑动结束后,接着触发click,导致事件混乱而出错
 | ||
| 				moving: false,
 | ||
| 			};
 | ||
| 		},
 | ||
| 		watch: {
 | ||
| 			value(val) {
 | ||
| 				this.activeIndex = val;
 | ||
| 			},
 | ||
| 			activeIndex: 'emitEvent'
 | ||
| 		},
 | ||
| 		methods: {
 | ||
| 			init() {
 | ||
| 				uni.$u.sleep().then(() => {
 | ||
| 					this.getRateItemRect();
 | ||
| 					this.getRateIconWrapRect();
 | ||
| 				})
 | ||
| 			},
 | ||
| 			// 获取评分组件盒子的布局信息
 | ||
| 			async getRateItemRect() {
 | ||
| 				await uni.$u.sleep();
 | ||
| 				// uView封装的获取节点的方法,详见文档
 | ||
| 				// #ifndef APP-NVUE
 | ||
| 				this.$uGetRect("#" + this.elId).then((res) => {
 | ||
| 					this.rateBoxLeft = res.left;
 | ||
| 				});
 | ||
| 				// #endif
 | ||
| 				// #ifdef APP-NVUE
 | ||
| 				dom.getComponentRect(this.$refs["u-rate"], (res) => {
 | ||
| 					this.rateBoxLeft = res.size.left;
 | ||
| 				});
 | ||
| 				// #endif
 | ||
| 			},
 | ||
| 			// 获取单个星星的尺寸
 | ||
| 			getRateIconWrapRect() {
 | ||
| 				// uView封装的获取节点的方法,详见文档
 | ||
| 				// #ifndef APP-NVUE
 | ||
| 				this.$uGetRect("." + this.elClass).then((res) => {
 | ||
| 					this.rateWidth = res.width;
 | ||
| 				});
 | ||
| 				// #endif
 | ||
| 				// #ifdef APP-NVUE
 | ||
| 				dom.getComponentRect(
 | ||
| 					this.$refs["u-rate__content__item__icon-wrap"][0],
 | ||
| 					(res) => {
 | ||
| 						this.rateWidth = res.size.width;
 | ||
| 					}
 | ||
| 				);
 | ||
| 				// #endif
 | ||
| 			},
 | ||
| 			// 手指滑动
 | ||
| 			touchMove(e) {
 | ||
| 				// 如果禁止通过手动滑动选择,返回
 | ||
| 				if (!this.touchable) {
 | ||
| 					return;
 | ||
| 				}
 | ||
| 				this.preventEvent(e);
 | ||
| 				const x = e.changedTouches[0].pageX;
 | ||
| 				this.getActiveIndex(x);
 | ||
| 			},
 | ||
| 			// 停止滑动
 | ||
| 			touchEnd(e) {
 | ||
| 				// 如果禁止通过手动滑动选择,返回
 | ||
| 				if (!this.touchable) {
 | ||
| 					return;
 | ||
| 				}
 | ||
| 				this.preventEvent(e);
 | ||
| 				const x = e.changedTouches[0].pageX;
 | ||
| 				this.getActiveIndex(x);
 | ||
| 			},
 | ||
| 			// 通过点击,直接选中
 | ||
| 			clickHandler(e, index) {
 | ||
| 				// ios上,moving状态取消事件触发
 | ||
| 				if (uni.$u.os() === "ios" && this.moving) {
 | ||
| 					return;
 | ||
| 				}
 | ||
| 				this.preventEvent(e);
 | ||
| 				let x = 0;
 | ||
| 				// 点击时,在nvue上,无法获得点击的坐标,所以无法实现点击半星选择
 | ||
| 				// #ifndef APP-NVUE
 | ||
| 				x = e.changedTouches[0].pageX;
 | ||
| 				// #endif
 | ||
| 				// #ifdef APP-NVUE
 | ||
| 				// nvue下,无法通过点击获得坐标信息,这里通过元素的位置尺寸值模拟坐标
 | ||
| 				x = index * this.rateWidth + this.rateBoxLeft;
 | ||
| 				// #endif
 | ||
| 				this.getActiveIndex(x,true);
 | ||
| 			},
 | ||
| 			// 发出事件
 | ||
| 			emitEvent() {
 | ||
| 				// 发出change事件
 | ||
| 				this.$emit("change", this.activeIndex);
 | ||
| 				// 同时修改双向绑定的value的值
 | ||
| 				this.$emit("input", this.activeIndex);
 | ||
| 			},
 | ||
| 			// 获取当前激活的评分图标
 | ||
| 			getActiveIndex(x,isClick = false) {
 | ||
| 				if (this.disabled || this.readonly) {
 | ||
| 					return;
 | ||
| 				}
 | ||
| 				// 判断当前操作的点的x坐标值,是否在允许的边界范围内
 | ||
| 				const allRateWidth = this.rateWidth * this.count + this.rateBoxLeft;
 | ||
| 				// 如果小于第一个图标的左边界,设置为最小值,如果大于所有图标的宽度,则设置为最大值
 | ||
| 				x = uni.$u.range(this.rateBoxLeft, allRateWidth, x) - this.rateBoxLeft
 | ||
| 				// 滑动点相对于评分盒子左边的距离
 | ||
| 				const distance = x;
 | ||
| 				// 滑动的距离,相当于多少颗星星
 | ||
| 				let index;
 | ||
| 				// 判断是否允许半星
 | ||
| 				if (this.allowHalf) {
 | ||
| 					index = Math.floor(distance / this.rateWidth);
 | ||
| 					// 取余,判断小数的区间范围
 | ||
| 					const decimal = distance % this.rateWidth;
 | ||
| 					if (decimal <= this.rateWidth / 2 && decimal > 0) {
 | ||
| 						index += 0.5;
 | ||
| 					} else if (decimal > this.rateWidth / 2) {
 | ||
| 						index++;
 | ||
| 					}
 | ||
| 				} else {
 | ||
| 					index = Math.floor(distance / this.rateWidth);
 | ||
| 					// 取余,判断小数的区间范围
 | ||
| 					const decimal = distance % this.rateWidth;
 | ||
| 					// 非半星时,只有超过了图标的一半距离,才认为是选择了这颗星
 | ||
| 					if (isClick){
 | ||
| 						if (decimal > 0) index++;
 | ||
| 					} else {
 | ||
| 						if (decimal > this.rateWidth / 2) index++;
 | ||
| 					}
 | ||
| 
 | ||
| 				}
 | ||
| 				this.activeIndex = Math.min(index, this.count);
 | ||
| 				// 对最少颗星星的限制
 | ||
| 				if (this.activeIndex < this.minCount) {
 | ||
| 					this.activeIndex = this.minCount;
 | ||
| 				}
 | ||
| 
 | ||
| 				// 设置延时为了让click事件在touchmove之前触发
 | ||
| 				setTimeout(() => {
 | ||
| 					this.moving = true;
 | ||
| 				}, 10);
 | ||
| 				// 一定时间后,取消标识为移动中状态,是为了让click事件无效
 | ||
| 				setTimeout(() => {
 | ||
| 					this.moving = false;
 | ||
| 				}, 10);
 | ||
| 			},
 | ||
| 		},
 | ||
| 		mounted() {
 | ||
| 			this.init();
 | ||
| 		},
 | ||
| 	};
 | ||
| </script>
 | ||
| 
 | ||
| <style lang="scss" scoped>
 | ||
| @import "../../libs/css/components.scss";
 | ||
| $u-rate-margin: 0 !default;
 | ||
| $u-rate-padding: 0 !default;
 | ||
| $u-rate-item-icon-wrap-half-top: 0 !default;
 | ||
| $u-rate-item-icon-wrap-half-left: 0 !default;
 | ||
| 
 | ||
| .u-rate {
 | ||
|     @include flex;
 | ||
|     align-items: center;
 | ||
|     margin: $u-rate-margin;
 | ||
|     padding: $u-rate-padding;
 | ||
|     /* #ifndef APP-NVUE */
 | ||
|     touch-action: none;
 | ||
|     /* #endif */
 | ||
| 
 | ||
|     &__content {
 | ||
|         @include flex;
 | ||
| 
 | ||
| 		&__item {
 | ||
| 		    position: relative;
 | ||
| 
 | ||
| 		    &__icon-wrap {
 | ||
| 		        &--half {
 | ||
| 		            position: absolute;
 | ||
| 		            overflow: hidden;
 | ||
| 		            top: $u-rate-item-icon-wrap-half-top;
 | ||
| 		            left: $u-rate-item-icon-wrap-half-left;
 | ||
| 		        }
 | ||
| 		    }
 | ||
| 		}
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| .u-icon {
 | ||
|     /* #ifndef APP-NVUE */
 | ||
|     box-sizing: border-box;
 | ||
|     /* #endif */
 | ||
| }
 | ||
| </style>
 | 
