活动抽奖组件设计
一 项目背景
略 公司内部资料以及安全红线 只讨论技术细节 不透漏相关交互和设计
二 交互以及视觉
整体视觉:
略
抽奖模块:大概是以一个花朵的花瓣为抽奖背景,旋转的时候不同的花瓣点亮,省略视觉稿,只讨论技术实现细节
交互:略
三 分析需要解决的问题
当做一个没有做过的东西的时候,这时候应该分析下完成这个效果需要考虑的问题,然后分解分析,然后逐步实施,分析总结遇到过的问题,是否有没考虑到的。
其实这种抽奖模块在现在的活动页中十分的常见,尤其是在活动页中。
首先,整理了TODO List
- 需要写成组件的形式,方便复用,需要考虑组件需要的传递的参数等等。
- 后端是接口的形式,也就是说,用户点击抽奖的时候,已经获得了中奖信息,前端需要执行一下抽奖轮转的动画,这个动画的时间、转动速率、转动期间禁止需要重复点击、初始的指针位置都需要前端来控制。
- 状态管理,针对不同抽奖次数、区分是否登陆、是否在APP内等相应的状态进行处理,弹窗或者跳转相应的URL
- 前端埋点相关和数据上报相关。
- 样式问题,设计交互的思路实际上是一个九宫格抽奖,但是视觉样式却是偏向指针类的轮盘抽奖,逻辑层面暂定的是按照九宫格的思路去实现,但是需要样式方面进行调整
- 抽奖的结果也需要写成一个弹窗组件,并且需要定义传入的参数
- 异常展示,当接口调用失败、后端数据库库存不足等等需要相应的warning弹窗组件
大致交互流程如下
四 具体设计
4.1 封装成公共组件,主要是对组件传递的参数进行定义,首先在首页需要调用抽奖组件,必传的有三个参数
showLuckDraw // 是否展示抽奖模块,如果用户已经抽过奖,默认不展示此模块,直接展示抽奖结果
userStatus // 用户状态:首页接口中会返回此信息,需要传入到组件中
drawStatus // 抽奖状态:
此外还需要给抽奖组件的子组件–抽奖结果组件传递参数
showSpecial&&type
props: { // 是否展示抽奖模块,如果用户已经抽过奖,默认不展示此模块,直接展示抽奖结果 showLuckDraw: { type: Boolean }, // 用户状态 userStatus: { type: Number, required: true }, // 抽奖状态 0: 跳转活动页 1: 需要通过活动增加抽奖次数 3: 不可抽奖 drawStatus: { type: Number, required: true }, // 是否已登陆 未登陆吊起登陆组件 hasLogin: { type: Boolean, required: true }, // 特殊弹窗 展示抽奖的结果 showSpecial: { type: Boolean, default: false }, // 向抽奖结果弹窗 传的值 显示对应的抽奖结果 type: { type: Number, required: true, } }, |
4.2 抽奖动画(核心逻辑)
核心的思路是,当点击抽奖的时候,调用后端接口,并且设置转盘所需要的初始参数,包括如下
- diff 速度累加变量,控制逐渐加快/逐渐减慢
- star 开始按钮点击态,如果用户已经无法抽奖,直接禁用
- speed 抽奖初始速度
-
isDisable 是否禁用开始抽奖按钮,防止重复抽奖,保证在在抽奖动画结束后才能再次抽奖,避免重复调用后端接口
-
drawStatus 设置抽奖状态初始值
- startTime 记录转动开始时间,用来控制动画时长
同时,设置一个定时器,轮盘的背景图片依次切换来实现动画的效果,利用定期器的milliseconds 时间间隔进行速度控制,当达到预设的转动时间时,开始做减速运动,且如果后端返回的奖品id和前端设置的奖品id能对应上时,清除定时器,同时return中止函数,且重新设置转盘的状态值,完成抽奖动画。
html 部分使用的简洁的jade,虽然刚开始上手有些不适应,但是用惯了之后,感觉十分简洁干净,不需要在大段的html代码里寻找calss等
<template lang="jade"> .award-box(v-if="showLuckDraw") .award-bg .award(:class="'active' + (current)") button.start-btn( @click="start", :disabled="isDisable", :class="star ? 'star': '' " ) .award-num 当前抽奖次数 i.award-n {{ currentNum = +this.drawStatus === 2 ? 1 : 0 }} i 次 special-dialog(:show-special="showSpecial", :type="id") </template> |
move() { let timeout = setTimeout( () => { this.current++; // 切换背景图,初始值可以随意设置 if ( this.current > 4) { // 因为只有5个奖品,所以当current 从0开始大于4的时候,重新加载第一张图片 this.current = 0; } // 若抽中的奖品id存在,且转动时间大于2秒,则开始减速转动 if ( this.award.id && ( Date.now() - this.startTime ) / 1000 > 2 ) { this.speed += this.diff; // 转动减速 //若转动超过4秒,并且奖品id等于格子的奖品id,则停下来 // this.awards 是从后端获取的中奖结果,如果和前端对应的奖品id对应上的话则为中奖 if ( ( Date.now() - this.startTime ) / 1000 > 4 && this.award.id == this.awards[this.current].id ) { clearTimeout( timeout ); this.star = false; this.isDisable = false; this.showSpecial = true; // 调用抽奖结果组件 this.type = this.id; // 将中奖结果赋值给type,显示对应的抽奖结果组件 setTimeout( () => { console.log(this.award.id); },0); return; // 抽中奖后停止定时器 } //若抽中的奖品不存在,则加速转动 } else { // todo:如果后端接口200且返回的中奖结果是错的,这里可以处理下异常 this.speed -= this.diff; } this.move(); }, this.speed ); }, |
4.3 状态管理,在调用move() 抽奖动画的之前,就对相应的用户状态做处理,未登陆的吊起登陆,没有次数的去获取次数,不可抽奖调用弹窗等等
4.4 埋点这里在工具函数里封装了一个公共的函数,直接调用传参记录埋点信息,关于埋点的话有空再展开
4.5 样式方面,遇到了一些问题
- 最开始的想法是只改变花瓣的背景图,但是最后发现,图片是不规则的,是有角度的,难以定位准确。而且定位位置的计算在不同机型上有差异,所以采用整体图片的替换
- 整体图片的替换又遇到一个闪烁问题:
成因:因为当图片花瓣旋转时视觉上只是每个花瓣的变化,实际上是整张图片的变化,通过动态的切换calss来完成背景图片的替换。正因为这样,新的问题就产生了,当代码构建部署打包发不到线上时,静态资源一般会有单独的服务器,也就是说图片的资源请求会有一定延迟,这就导致了第一次加载点击抽奖的时候,页面花瓣转动会有闪烁,而第二次图片会走缓存,所以这个问题只出现一次,但是却是致命的
解决方法:把url改为base64的Data URL,实际上就是利用base64编码把图片数据翻译成标准ASCII字符,Data URL是在本地直接绘制图片,不是从服务器加载,所以节省了HTTP连接,起到加速网页的作用,但是弊端就是IE8以下浏览器不支持这种方法(当然app内肯定是支持的,绝大部分手机也是支持的)。用这种方法会加重客户端的CPU和内存负担,总之有利有弊,而且不适合大图片,针对这个图片找UI替换了体积更小的图片,以减轻客户端的内存负担。
4.6 针对中奖结果的弹窗,封装另一个组件,只需要传递type的参数就显示不同的中奖结果,类似的还有挂在到全局的弹窗组件,异常的时候及时抛出错误弹窗
五 总结和反思
虽然说现在活动页比较多且交互复杂,但是可以尽可能的封装一些公共的组件,提供基本的骨架,只需要修改一些css就能快速完成一个功能。
还有对于没有做过的东西要具体分析,拆解成步骤,一步一步的完成,同时预留好一定的buffer时间,最重要的,经常总结与反思,查找自身的不足,在繁忙的工作中提升自己。