手把手教你实现九宫格抽奖活动

1,089 阅读1分钟

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

动画2.gif

根据步骤来,一步步教你实现一个简单的九宫格抽奖动画

创建项目,引入vue

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
  <div id="app">
  </div>
</body>

<script type="text/javascript">

const app = {
  data() {
    return {}
  },
  mounted(){
  },
  methods:{}

}

Vue.createApp(app).mount('#app')

</script>
</html>

静态页面

先画底盘

主要通过两个div叠加
image.png

<head>
+ <style>
+ * {
+   box-sizing: border-box;
+   padding: 0;
+   margin: 0;
+ }
+ .grid-box{
+   width: 400px;
+   height: 400px;
+   border-radius: 16px;
+   background-color: #617df2;
+   margin: 100px auto;
+   padding: 15px;
+ }
+ .grid-box__inner{
+   width: 100%;
+   height: 100%;
+   border-radius: 12px;
+   background-color: #869cfa;
+ }
+ </style>
</head>

<body>
  <div id="app">
+     <div class="grid-box">
+       <div class="grid-box__inner">
+       </div>
+     </div>
  </div>
</body>

画奖项

通过 v-for 遍历奖项数组,利用flex布局进行排版

image.png

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://unpkg.com/vue@next"></script>
  <style>
    * {
      box-sizing: border-box;
      padding: 0;
      margin: 0;
    }
    .grid-box{
      width: 400px;
      height: 400px;
      border-radius: 16px;
      background-color: #617df2;
      margin: 100px auto;
      padding: 15px;
    }
    .grid-box__inner{
      width: 100%;
      height: 100%;
      border-radius: 12px;
      background-color: #869cfa;

+      display: flex;
+      flex-flow: row wrap;
+      align-content:flex-start 
    }
+    .grid-box__item{
+      width: 103px; 
+      height: 103px;
+      margin-top: 15px;
+      margin-left: 15px;

+      background-color: #b8c5f2;
+      border-radius: 8px;
+      color: #fff;

+      display: flex;
+      justify-content:center;
+      align-items:center;
+    }
+     .grid-box__item.start-btn{
+      cursor:pointer;
+    }

  </style>
</head>
<body>
  <div id="app">
    <div class="grid-box">
      <div class="grid-box__inner">
+        <div class="grid-box__item prize" v-for="(prize,i) in list" :key="'prize'+i">
+          {{ prize.label }}
+        </div>
+        <div class="grid-box__item start-btn">
+          开始
+        </div>
      </div>
    </div>
  </div>
</body>

<script type="text/javascript">

const app = {
  data() {
    return {
+      list: [  
+        { label: '一等奖' },
+        { label: '二等奖' },
+        { label: '三等奖' },
+        { label: '安慰奖' },
+        { label: '谢谢参与' },
+        { label: '安慰奖' },
+        { label: '谢谢参与' },
+        { label: '三等奖' },
      ]
    }
  },
  mounted(){
  },
  methods:{}

}

Vue.createApp(app).mount('#app')

</script>
</html>

注意:此时奖项的顺序是按每行从头开始排的,开始按钮也不再最中间。因此进行一下小修改

画奖项(修改版)

先让所有奖项居中

image.png
移除 flex 布局,利用 position:absolute 实现居中

.grid-box__inner{
  width: 100%;
  height: 100%;
  border-radius: 12px;
  background-color: #869cfa;

+ position: relative;
- display: flex;
- flex-flow: row wrap;
- align-content:flex-start
}
.grid-box__item{

+  position: absolute;
+  left: 50%;
+  top: 50%;
+  margin-left: -51.5px;
+  margin-top: -51.5px;

  width: 103px; 
  height: 103px;
-  margin-top: 15px;
-  margin-left: 15px;

  background-color: #b8c5f2;
  border-radius: 8px;
  color: #fff;

  display: flex;
  justify-content:center;
  align-items:center;
}

对每个奖项进行位移

通过transform:translate(x,y)来进行位移 位移距离为 一个奖项的宽高+间距

image.png

<body>
  <div id="app">
    <div class="grid-box">
      <div class="grid-box__inner">
-        <div class="grid-box__item prize" v-for="(prize,i) in list" :key="'prize'+i">
+        <div class="grid-box__item prize" v-for="(prize,i) in list"  :style="getGridItemStyle(i)" :key="'prize'+i">
          {{ prize.label }}
        </div>
        <div class="grid-box__item start-btn">
          开始
        </div>
      </div>
    </div>
  </div>
</body>

<script type="text/javascript">
const app = {
  methods:{
+    getGridItemStyle(index){
+       const gutter = 15 // 间距
+       const width_height = 103 // 宽度 | 高度
+       const margin = gutter + width_height // 偏移量

+       const map = {
+         0:`translate(-${margin}px,-${margin}px)`,
+         1:`translate( 0px,-${margin}px)`,
+         2:`translate( ${margin}px,-${margin}px)`,
+         3:`translate( ${margin}px,0px)`,
+         4:`translate(${margin}px,${margin}px)`,
+         5:`translate(0px,${margin}px)`,
+         6:`translate(-${margin}px,${margin}px)`,
+         7:`translate(-${margin}px,0px)`,
+       }

+       return {
+         transform:map[index] || ''
+       }
    }
  }
}
</script>

选中样式

image.png

通过切换class来实现选中样式

<style>
+   .grid-box__item.active{
+     background-color: #7074f6;
+   }
</style>
<body>
  <div id="app">
    <div class="grid-box">
      <div class="grid-box__inner">
-       <div class="grid-box__item prize" v-for="(prize,i) in list" :style="getGridItemStyle(i)" :key="'prize'+i">
+       <div class="grid-box__item prize" :class="{active:curIndex === i}" v-for="(prize,i) in list" :style="getGridItemStyle(i)" :key="'prize'+i">
          {{ prize.label }}
        </div>
        <div class="grid-box__item start-btn">
          开始
        </div>
      </div>
    </div>
  </div>
</body>

<script type="text/javascript">

const app = {
  data() {
    return {
+      curIndex:0,
    }
  },
}
</script>

旋转动画

思路:通过定时器setTimeout改变curIndex的值来实现动画

绑定点击事件

- <div class="grid-box__item start-btn">
+ <div class="grid-box__item start-btn" @click="handleClickStartBtn">
  开始
</div>


<script type="text/javascript">

const app = {
  data() {
    return {
+      isTurning:false,
    }
  },
  methods:{
+     handleClickStartBtn(){
+      if(this.isTurning) return
+      this.turn()
+    },
+    turn(){}
  }
}
</script>

模拟中奖

<script type="text/javascript">

const app = {
  methods:{
+     getWinningIndex(){
+      return parseInt(Math.random() * 8, 10)
+    },
  }
}
</script>

抽奖动画

动画2.gif

<script type="text/javascript">

const app = {
  methods:{
    turn(){
+      const winningIndex = this.getWinningIndex()
+      let index = 0
+      this.curIndex = 0
+      this.isTurning = true

+      let setTime = ()=>{
+        setTimeout(()=>{
+          // 至少转3圈
+          if(this.curIndex==winningIndex && parseInt(index/8)===3){
+            console.log('中奖了,奖品为:'+ this.list[this.curIndex].label)
+            this.isTurning = false
+          }else{
+            index++
+            this.curIndex = index%8
+            setTime()
+          }
+        },200)
+      }

+      setTime()
    },
  }
}
</script>

抽奖动画(变速)

动画2.gif
累计动画跳动的次数,通过判断当前跳动次数到达指定奖项需要跳动的总次数,当差值小于指定次数时开始减速。通过动态设置 setTimeout 的 time 来实现减速

<script type="text/javascript">

const app = {
  methods:{
    turn(){
      const winningIndex = this.getWinningIndex()

      this.curIndex = 0 // 重置curIndex
      this.isTurning = true


      const lastSpeed = 600 // 最终速度( 速度 ==> setTimeout 的 延迟时间)
      const startSpeed = 80 // 初始速度
      const count = 4 // 转的圈数
      const speedDownNum = 15 // 倒数第 speedDownNum 次开始减速
      const speed = (lastSpeed - startSpeed) / speedDownNum // 平均每次减速的量

      let index = 0 // 变换的次数
      let time = startSpeed // setTimeout 的 延迟时间

      // 通过动态修改 setTimeout 的延迟时间 来达到变换速度的效果
      let setTime = ()=> {
        setTimeout(() => {
          // 判断是否开始减速
          if (index >= count * 8 + winningIndex - speedDownNum) {
            time += speed
          }

          index++
          this.curIndex = index % 8

          // 当 index 小于 总数时 设置下一次 timeout
          if (index < count * 8 + winningIndex) {
            setTime()
          } else {
            this.isTurning = false
            console.log('中奖了,奖品为:'+ this.list[this.curIndex].label)
          }
        }, time)
      }

      setTime()
    },
  }
}
</script>

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://unpkg.com/vue@next"></script>
  <style>
    * {
      box-sizing: border-box;
      padding: 0;
      margin: 0;
    }
    .grid-box{
      width: 400px;
      height: 400px;
      border-radius: 16px;
      background-color: #617df2;
      margin: 100px auto;
      padding: 15px;
    }
    .grid-box__inner{
      width: 100%;
      height: 100%;
      border-radius: 12px;
      background-color: #869cfa;

      position: relative;
    }
    .grid-box__item{

      position: absolute;
      left: 50%;
      top: 50%;
      margin-left: -51.5px;
      margin-top: -51.5px;

      width: 103px; 
      height: 103px;

      background-color: #b8c5f2;
      border-radius: 8px;
      color: #fff;

      display: flex;
      justify-content:center;
      align-items:center;
    }
    .grid-box__item.active{
      background-color: #7074f6;
    }
    .grid-box__item.start-btn{
      cursor:pointer;
    }

  </style>
</head>
<body>
  <div id="app">
    <div class="grid-box">
      <div class="grid-box__inner">
        <div class="grid-box__item prize" :class="{active:curIndex === i}" v-for="(prize,i) in list" :style="getGridItemStyle(i)" :key="'prize'+i">
          {{ prize.label }}
        </div>
        <div class="grid-box__item start-btn" @click="handleClickStartBtn">
          开始
        </div>
      </div>
    </div>
  </div>
</body>

<script type="text/javascript">

const app = {
  data() {
    return {
      list: [  
        { label: '一等奖' },
        { label: '二等奖' },
        { label: '三等奖' },
        { label: '安慰奖' },
        { label: '谢谢参与' },
        { label: '安慰奖' },
        { label: '谢谢参与' },
        { label: '三等奖' },
      ],
      curIndex:null,
      isTurning:false
    }
  },
  mounted(){
  },
  methods:{
    getGridItemStyle(index){
      const gutter = 15 // 间距
      const width_height = 103 // 宽度 | 高度
      const margin = gutter + width_height

      const map = {
        0:`translate(-${margin}px,-${margin}px)`,
        1:`translate( 0px,-${margin}px)`,
        2:`translate( ${margin}px,-${margin}px)`,
        3:`translate( ${margin}px,0px)`,
        4:`translate(${margin}px,${margin}px)`,
        5:`translate(0px,${margin}px)`,
        6:`translate(-${margin}px,${margin}px)`,
        7:`translate(-${margin}px,0px)`,
      }

      return {
        transform:map[index] || ''
      }
    },
    handleClickStartBtn(){
      if(this.isTurning) return
      this.turn()

    },
    turn(){
      const winningIndex = this.getWinningIndex()

      this.curIndex = 0
      this.isTurning = true


      const lastSpeed = 600 // 最终速度( 速度 ==> setTimeout 的 延迟时间)
      const startSpeed = 80 // 初始速度
      const count = 4 // 转的圈数
      const speedDownNum = 15 // 倒数第 speedDownNum 次开始减速
      const speed = (lastSpeed - startSpeed) / speedDownNum // 平均每次减速的量

      let index = 0 // 变换的次数
      let time = startSpeed // setTimeout 的 延迟时间

      // 通过动态修改 setTimeout 的延迟时间 来达到变换速度的效果
      let setTime = ()=> {
        setTimeout(() => {
          // 判断是否开始减速
          if (index >= count * 8 + winningIndex - speedDownNum) {
            time += speed
          }

          index++
          this.curIndex = index % 8

          // 当 index 小于 总数时 设置下一次 timeout
          if (index < count * 8 + winningIndex) {
            setTime()
          } else {
            this.isTurning = false
            console.log('中奖了,奖品为:'+ this.list[this.curIndex].label)
          }
        }, time)
      }

      setTime()

    },
    getWinningIndex(){
      return parseInt(Math.random() * 8, 10)
    },
  }

}

Vue.createApp(app).mount('#app')

</script>
</html>

码上掘金