让CSS3中Transform属性带你一文实现炫酷的转盘抽奖效果

1,602 阅读5分钟

前端时间有个需求是客户端双端APP内嵌入整个转盘抽奖的web子系统,具体是要在后台能够控制大转盘抽奖的奖项数,和用户免费抽奖的次数,并且免费抽奖使用完,用户可以观看广告进行抽奖或使用积分抽奖。正好最近有空,出了这篇教程,解析转盘抽奖的实现过程。

在这里插入图片描述

此子系统整体开发的话由我负责,其中前端技术:H5+CSS3+JS;后端技术:YII2+Redis。

点击查看转盘演示视频效果

下面分两个部分介绍下这个转盘抽奖部分:

一、前端样式及抽奖操作开发

关于前端展示主要使用Transform来实现,下面是关于Transform属性【大家可以去菜鸟教程等看下其详细用法,感觉这个属性效果很炫酷】的介绍,其中主要用到的是rotate(定义 2D 旋转,在参数中规定角度)、skewX(定义沿着 X 轴的 2D 倾斜转换)

以我们代码为例:在prize-list内我们先做出整个转盘的背景图,prize-reward内展示每一个奖品信息。在首次进入页面时,调用接口获取转盘奖品信息,如:名称、icon图、奖品ID等,然后循环组装div标签,组装完成后,根据返回的奖品信息计算奖品条数,然后设置Transform属性的rotate和skewX,因为背景颜色是间隔展示,所以展示的奖品数为偶数,最多展示的奖项最大值为12。当时做的时候没有找到每次赋值的规律,所以2-12的情景值,是我尝试出来的,到这里的话整个的样式其实就已经出来了,视频展示的为6个奖品项效果,接口返回的奖品条数不同,此页面展示不同效果。

此部分就是转盘部分的布局:

<div class="wheelSurf">
     <div class="wheel">
         <div class="wheel-icon">
             <img src="../images/wheel/wheel_back.png" alt="">
         </div>
         <div class="prize-box">
             <div class="prize-content">
         		 <!-- 转盘的背景盒子,JS填充奖品背景 -->
                 <div class="prize-list" id="prize-list"></div>
                 
         		 <!-- 转盘的奖品盒子,JS填充奖品 -->
                 <div class="prize-reward" id="prize-reward"></div>
             </div>
             
         	 <!-- 点击抽奖按钮 -->
             <div class="prize-button" id="prize-button">
                 <img src="../images/wheel/start_button.png" alt="">
             </div>
         </div>
         <div class="score">
             <div class="spend_score">
                 <span class="consume-score">0</span>积分/次
             </div>
             <div class="my_score">
                 我的积分:<span id="my-score">0</span>
             </div>
         </div>
     </div>
     <div class="activity_rule">
         <div class="rule_title">活动规则</div>
         <div class="rule_list" id="activity-rule"></div>
     </div>

 </div>

此部分就是转盘奖品展示的代码:

/**
* 大转盘填充奖项,初始化展示转盘背景
* @param data
*/
function prize(data) {
    let list = '';
    let reward = '';
    let count = data.length;
    let rotate = 360 / count;

    prizeCount = count;
    prizeDeg   = 360 / count;

    let itemRotate = 360 / 2 / count;
    let liRotate   = 0;

    // 设置4、6、8、10、12数量奖品的旋转角度值
    let width;
    switch (count) {
        case 2:
            liRotate   = 0;
            width = 4;
            break;
        case 4:
            liRotate   = 0;
            width = 3;
            break;
        case 6:
            liRotate   = 30;
            width = 2;
            break;
        case 8:
            liRotate   = 45;
            width = 1.5;
            break;
        case 10:
            liRotate   = 54;
            width = 1.3;
            break;
        case 12:
            liRotate   = 60;
            width = 1;
            break;
        default:
            liRotate   = 0;
            width = 1;
    }

	// 组装背景和奖品的标签,且设置奖品的旋转角度
    for(let i=0; i < count; i++) {
        list += "<div class=\"prize-li\"></div>";
        reward += "<div class=\"prize-item\" data-id=\"" + data[i]['id'] + "\" style=\"transform: rotate(" + (rotate*(i+1) - itemRotate) + "deg) translateX(-50%);width: " + width + "rem;\">\n" +
            "                                <div class=\"prize-name\">\n" +
            data[i]['title'] +
            "                                </div>\n" +
            "                                <div class=\"prize-icon\">\n" +
            "                                    <img src=\"" + data[i]['icon'] + "\">\n" +
            "                                </div>\n" +
            "                            </div>"
    }

	// 填充内容
    $('#prize-list').html(list);
    $('#prize-reward').html(reward);

	// 获取所以的背景标签,循环设置背景颜色及旋转的角度值和倾斜转换值
    [].slice.call(document.querySelectorAll('.prize-li'), 0).forEach(function (item, i) {
        if(count == 2) {
            return false;
        }
        
        item.style.backgroundColor = '#ffffff';
        if(i%2 == 0) {
            item.style.backgroundColor = '#FCE9C1';
        }
        
        item.style.transform = 'rotate(' + (360 / count * i + liRotate) + 'deg) skewX(' + liRotate +'deg)';
    });

	// 如果只有两个奖品则单独处理下样式
    if(count == 2) {
        $('#prize-list').find('.prize-li').css('width', '2.49rem');
        $('#prize-list').find('.prize-li').css('height', '4.98rem');
        $('#prize-list').find('.prize-li').css('top', '0rem');
        $('#prize-list').find('.prize-li').css('left', '0rem');
        $('#prize-list').find('.prize-li').eq(0).css('background', '#FCE9C1');
        $('#prize-list').find('.prize-li').eq(1).css('left', '2.49rem');
    }
}

接下来就是前端抽奖操作的实现,用户点击抽奖按钮进行抽奖,首先的话前端设置一个抽奖锁,在用点击抽奖按钮的时候将锁锁上,在这次抽奖过程完成后,将锁打开,用户点击抽奖,请求抽奖接口,然后接口进行抽奖逻辑处理,将最终的奖品信息返回给前端,也就是奖品的ID,然后前端根据ID拿到奖品的标签下标,前端使用transition方式渲染装盘,最终将指针停在转盘内接口返回的奖品那里,用户看到中奖信息进行下步操作。这样的话,整个前端抽奖过程就完成了

此部分是转盘抽奖部分代码

/**
 * 抽奖操作
 * @param type
 */
function lottery(type) {

	// 请求后端接口,获取中奖信息
   $.ajax({
       url: wheel_lottery,
       type: 'get',
       headers: {
           "token": token,
           "Accept": "application/json",
           "appid": appId
       },
       data: {
           type: type // 用户抽奖类型
       },
       success:function (res) {
       
           if (!res.data) {
               flag = true;
               return false;
           }

           var type    = res.data.type;
           var title   = res.data.title;
           var img     = res.data.img;
           var rid     = res.data.record_id;
           myScore     = res.data.my_score;
           pid         = res.data.prize_id;

           // 更新积分及抽奖次数等信息
           count--;
           $('#prize-count').html(count);

           // 获取当前奖项标签的下标,用于计算转盘转动的角度值
           var code = $("#prize-reward").find(".prize-item[data-id=" + pid + "]").index();

           if(code == -1) { // 未查询到标签
               flag = true;
               return false;
           }

           // 转盘转动,设置转盘转动的角度值
           var e = 3600 - (code * prizeDeg) - prizeDeg / 2;
           $('.prize-content').css({'transition': 'transform 6s cubic-bezier(0.25, 0.1, 0.01, 1)', 'transform': "rotate(" + e + "deg)"});

           setTimeout(function () { // 消息提示
               $('#mask').css('display', 'block');
               $('#my-score').html(myScore);
               if (type == 1) {
                   $('#prize-score').css('display', 'block');
                   $('#result-img').attr('src', img);
                   $('#result-score').html(title);
               } else if(type == 2) {
                   $('#prize-result').css('display', 'block');
                   $('#result-icon').attr('src', img);
                   $('#result-reward').html(title);
                   $('#go-write').attr('data-id', rid)
               } else {
                   $('#prize-no').css('display', 'block');
               }

               // 转盘复位
               $('.prize-content').css({'transition': '','transform': "rotate(0deg)"});

			   // 打开抽奖锁
               flag = true;
           }, 6000);
       },
       error:function(res) {
       }
   })
}

二、后端接口抽奖逻辑开发

其实关于设置展示的奖项条数、获取奖项数据这些接口没啥要讲的,就是一般的增删改查操作,其中有一点需要特别强调下,一般的抽奖系统是能够控制每个奖项的中奖概率的,这里我们通过设置weight【权重】字段值来控制中奖概率,weight值越大中奖几率越高。

确定中奖奖品代码:

  /**
   * 抽奖方法,返回中奖商品id
   *
   * @return mixed
   */
  private function _getLotteryPrize()
  {
      // 所有奖品的权重和id
      $prize = WheelAwardPrize::select('id', 'weight')
                              ->orderBy('created_at', 'desc')
                              ->limit(Tenancy::setting('wheel.prize_num'))
                              ->get();

      $data = []; 		// 最终抽奖奖品数组
      $totalWeight = 0; // 初始化总权重

      foreach ($prize as $item) {
          $weight = $item['weight']; // 当前奖品权重值
          if (!$weight) { 			 // 没有权重值跳过
              continue;
          }

          for ($i = 1; $i <= $weight; $i++) { // 循环添加进最终数组
              $data[] = $item;
          }
          $totalWeight += $weight; // 增加总权重
      }

	  // 随机获取中奖奖品下标
      $index = rand(0, $totalWeight - 1);
		
	  // 返回奖品ID
      return $data[$index]['id'];
  }

下面就主要讲一下抽奖逻辑的实现:首先和前端一样,设置抽奖锁,完成抽奖记得释放锁,同一个用户完成一次抽奖流程后,才能允许进行下一次抽奖。然后确定用户中奖奖品,从库中查询全部奖品ID【奖品主键值】和weight【权重】,然后循环weight,将当前weight的ID和weight放置在一个二维数组内,这样我们就能拿到一个不存放weight为0,且长度为weight总值的奖品数组,然后根据数组长度随机取一个范围内的值,这样我们就拿到当前index的值,进而拿到中奖奖品的ID。最后的话,就是根据中奖奖品ID,生成用户的中奖信息,如果是虚拟货币的话,就直接给用户充值,实物奖励的话,前端就提示用户填写收货信息,在把奖品ID和用户中奖记录的ID通过接口返回给前端。

抽奖流程代码:

/**
 * 进行抽奖,返回奖品ID
 *
 * @return array
 */
public function lottery(Request $request)
{
    // 加锁
    $lockKey = RedisKey::getApiLockKey('wheel/lottery', ['member_id' => auth()->user()->member_id]);
    if (Cache::has($lockKey)) { // 有锁
        throw new BusinessException('您的操作太过频繁,请稍后重试');
    }

    // 抽奖操作,获取中奖奖项ID
    $pid = $this->_getLotteryPrize();
    if (!$pid) {
        throw new BusinessException('奖品正在准备中,请稍后重试');
    }

    // 添加奖励记录
    $records = new WheelAwardRecord();
    $records->member_id = auth()->user()->member_id;
    $records->wheel_id  = $pid;
    $records->type      = $request->type;
    $records->date      = date('Ymd');

    $asset = Asset::where('member_id', auth()->user()->member_id)->first();
    if ($request->type == 2) { // 积分抽奖
    
        $config = Tenancy::setting('wheel');
        if (!$asset || ($asset->score < $config['consume_score'])) {
            Cache::forget($lockKey);
            throw new BusinessException('积分不足');
        }
        
        // 扣除用户积分
        $this->user->changeScore(-$config['consume_score'], ScoreReason::REASON_WHEEL, Task::TASK_WHEEL);
    }

    // 发放奖励
    $prizes = WheelAwardPrize::where('id', $pid)->first();

    if ($prizes->type == 1) { // 奖励虚拟货币
        $records->status = 3;
        
        // 增加用户积分
        $this->user->changeScore($config['consume_score'], ScoreReason::REASON_WHEEL, Task::TASK_WHEEL);
    } else if($prizes->type == 2) { // 实物奖励
        $records->status = 1; // 默认状态待发放
    } else { // 未中奖
        $records->status = 3; // 默认状态待发放
    }

    $records->save();

    // 移除锁
    Cache::forget($lockKey);

    return [
        'prize_id'  => $pid,
        'type'      => $prizes->type,
        'title'     => $prizes->title,
        'img'       => $prizes->icon,
        'record_id' => $records->id,
        'my_score'  => 600
    ];
}

不知不觉来北京已经一年半,明天就要结束北漂的生活,准备回老家休息一段时间,然后10号左右去上海找工作。

因为我现在做的是小说阅读方面的开发,所以趁着休息的这段时间出一篇小说阅读器的博文,大概是《一文带你掌握H5小说阅读器的开发,仿起点读书阅读器》,解析小说阅读器的开发过程,这个开发的话是准备仿照起点阅读阅读器开发,从0-1搭建小说阅读器。