u-rate.vue 9.88 KB
   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
<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>