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