| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 | <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: `0 ${$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: `0 ${$u.addUnit(gutter / 2)}`                        }"              :size="size"          ></u-icon>        </view>      </view>    </view>  </view></template><script>import props from './props.js';// #ifdef APP-NVUEconst 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 {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) {        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>
 |