|
|
<template> <view class="u-rate" :id="elId" ref="u-rate" :style="[addStyle(customStyle)]" > <view class="u-rate__content" @touchmove.stop="touchMove" @touchend.stop="touchEnd" > <view class="u-rate__content__item cursor-pointer" 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: `0 ${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: 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: `0 ${addUnit(gutter / 2)}` }" :size="size" ></u-icon> </view> </view> </view> </view> </template>
<script> import { props } from './props'; import { mpMixin } from '../../libs/mixin/mpMixin'; import { mixin } from '../../libs/mixin/mixin'; import { addUnit, addStyle, guid, sleep, range, os } from '../../libs/function/index'; // #ifdef APP-NVUE
const dom = weex.requireModule("dom"); // #endif
/** * rate 评分 * @description 该组件一般用于满意度调查,星型评分的场景 * @tutorial https://ijry.github.io/uview-plus/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: [mpMixin, mixin, props], data() { return { // 生成一个唯一id,否则一个页面多个评分组件,会造成冲突
elId: guid(), elClass: guid(), rateBoxLeft: 0, // 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
// #ifdef VUE3
activeIndex: this.modelValue, // #endif
// #ifdef VUE2
activeIndex: this.value, // #endif
rateWidth: 0, // 每个星星的宽度
// 标识是否正在滑动,由于iOS事件上touch比click先触发,导致快速滑动结束后,接着触发click,导致事件混乱而出错
moving: false, }; }, watch: { // #ifdef VUE3
modelValue(val) { this.activeIndex = val; }, // #endif
// #ifdef VUE2
value(val) { this.activeIndex = val; }, // #endif
activeIndex: 'emitEvent' }, // #ifdef VUE3
emits: ['update:modelValue', 'change'], // #endif
methods: { addStyle, addUnit, init() { sleep().then(() => { this.getRateItemRect(); this.getRateIconWrapRect(); }) }, // 获取评分组件盒子的布局信息
async getRateItemRect() { await 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 (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); // 同时修改双向绑定的值
// #ifdef VUE3
this.$emit("update:modelValue", this.activeIndex); // #endif
// #ifdef VUE2
this.$emit("input", this.activeIndex); // #endif
}, // 获取当前激活的评分图标
getActiveIndex(x,isClick = false) { if (this.disabled || this.readonly) { return; } // 判断当前操作的点的x坐标值,是否在允许的边界范围内
const allRateWidth = this.rateWidth * this.count + this.rateBoxLeft; // 如果小于第一个图标的左边界,设置为最小值,如果大于所有图标的宽度,则设置为最大值
x = 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>
|