九宫格抽奖,包括幸运大转盘,已经在不管是电商还是日常网页中已经算是比较常见的页面效果了(此处点名批评pdd),那么如何用Vue来实现九宫格以及后续的十二宫格,十六宫格等多宫格抽奖组件呢?

        先上效果图:

        

 一、转圈抽奖实现思路

        要实现九宫格抽奖,得先明确几个实现的要点:

  1. 得有个先加速到最快,点击停止后再逐渐减速的过程
  2. 允许内定选项存在(不管什么时候点击停止光块总会停止到指定选项)
  3. 有部分需求可能会规定转到第几圈开始加速

        所以根据这些情况,我们可以拟定代码如下:

    // 使转动停止的方法
    aroundStop() {
      // 清除定时器
      clearTimeout(this.timer);
      this.timer = null;
      return null;
    },

    aroundFunction(time,type) {
      if (time === 500) {
        // defaultId: 内定选项的Id
        if (this.defaultId && this.defaultId !== "") {
          if (this.defaultId === this.activeCell) return this.aroundStop();
        } else {
          return this.aroundStop();
        }
      }
      this.timer = setTimeout(() => {
        // type=true 的时候,表示开始转动,每次运行都会缩减延时器间隔,导致转动越来越快
        // type=false 的时候,表示停止转动,每次运行都会延长延时器间隔,导致转动越来越慢
        let ms = type ? time - 30 : time + 30;
        // activeOrder 是指示器指到的选项的下标
        // cellCount 是带选项的总个数,因为要对应下标所以-1
        if (this.activeOrder < this.cellCount - 1) {
          this.activeOrder = this.activeOrder + 1;
        } else {
          // 转到头就重置状态,circleNum 也就是圈数,此时圈数+1
          this.activeOrder = 0;
          this.circleNum = this.circleNum + 1;
        }
        // 转动效果最好的间隔的最小值
        if (ms < 50) {
          ms = 50;
        }
        // 转动间隔的最大值
        if (ms > 500) {
          ms = 500;
        }
        this.aroundFunction(ms, type);
      }, time);
    },

        其实也能看出来,实现很简单,就是根据点击的是开始还是结束来使用延时器进行光块的加速或者减速甚至停止。点击开始就将时间间隔从500每次减50,此时是转动加速阶段,一直减少到50ms,并保持不变,此时,转动进入稳定阶段。点击结束就将间隔从50开始,每次加50ms一直加到500ms,加到500ms后停止并清除定时器,此时转动进入减速阶段直至停止。

        至于为什么是使用延时器而不是定时器,是因为延时器能够和递归结合起来进行更好的逻辑处理,而且不用像定时器那样每次转动动作开始前都要进行定时器的清理。

         而且判定停止和加速的因素可以是很多,比如转到第三圈才开始加速,就将代码中的circleNum进行一个判断,判断其是不是大于等于三来作为是否减小延时器间隔的依据之一,内定的实现方式也是一样,当减速到恒定值的时候,每次转动判断此时转动到的奖品是不是内定值来确认是不是要停止转动。

        然而如果想完整的实现该组件,难点并不在于怎样进行功能的实现,而是怎样对九宫格,还有九宫格个扩展的4X4十二宫格,5X5十六宫格UI层面的搭建。

二、实现九宫格及多宫格的UI布局

     1.流行布局方式介绍

        其实如果仅仅是九宫格,则实现起来比较简单,那就是将对应的每个方块使用绝对定位的方式进行拼凑,强行凑出九宫格的布局,像这样:

        

         或者这样:

                

          或者是这样:

         上图来源于九宫格,大转盘抽奖开源组件lucky-canvas,官网地址:

九宫格抽奖 | 基于 Js / TS / Vue / React / 微信小程序 / uni-app / Taro 的【大转盘 & 九宫格 & 老虎机】抽奖插件🎉基于 Js / TS / Vue / React / 微信小程序 / uni-app / Taro 的【大转盘 & 九宫格 & 老虎机】抽奖插件,🎨奖品 / 文字 / 图片 / 颜色 / 按钮均可配置,支持同步 / 异步抽奖,🏅概率前 / 后端可控,可根据 dpr 自动调整清晰度适配移动端https://100px.net/demo/grid.html        我们可以看出这种布局方式都得对每个奖品进行单独的位置设置,九宫格还好,除开中间,设置八个就行了,倘若是12宫格(4X4),16宫格(5X5),20宫格(6X6)等多宫格,岂不是要分别设置12个,16个,20个甚至更多的位置呢?这显然不符合我们的期望。

        所以我们可以使用下面这个布局。

     2.flex布局实现多宫格

        经过实践证明,flex布局完全是可以实现九宫格,十二宫格,甚至十六宫格一直到N宫格的所有布局的:

        

         可以自由设置并完美兼容3X3一直到nXn的多宫格布局!
        但是flex布局固然好,但是有个致命的问题:

        flex布局里面元素的顺序是顺次的,体现在多宫格上就是Z型分布。

         可我们常见的九宫格里元素的顺序却是这样的圈型分布:

        

         两种分布方式的异同以及如何从z型分布变成圈型分布的方法因为本篇篇幅原因就不展开说了,想了解的请戳我这篇文章:

从多宫格的Z型布局到圈式布局_YOLO浪漫收藏家的博客-CSDN博客对于在九宫格格式下两种布局方式改变的探索https://blog.csdn.net/qq_39958056/article/details/127554439?spm=1001.2014.3001.5502        总之我们已经实现了元素在多宫格内的圈型分布:

         接下来的就很简单了,对元素进行分组,生成一个二维数组,然后对二维数组进行遍历生成九宫格,然后使用flex布局进行样式调整。

        就比如九宫格里面包含八个元素,可以生成一个大数组里面包含三个数量分别是3,2,3的三个小数组:

        

         如上图,进行页面渲染时的数据应该是:

[
  ['宇宙战将1','白起2','太阳系级宇宙战舰3'],
  ['大西洋级蘸酱8','小破木船4'],
  ['太阳级蘸酱7','月球级蘸酱6','地球级宇宙战将5']
]

        4X4,5X5等多宫格同理。 

        要想实现这个效果,我们首先得探寻3X3一直到nXn的布局中的规律:

        3*3 (8个元素)→ 【3个,2个,3个】

        4*4 (12个元素)→ 【4个,2个,2个,4个】

        5*5 (16个元素)→ 【5个,2个,2个,2个,5个】

        ……

        n*n (?个元素)→ 【n个,2个,2个,……,2个,n个】

        通过以上的规律可以看出,当为3x3的时候,中间长度为2的数组的个数是一个,4x4的时候为两个,5x5的时候为三个,所以当nxn的时候中间长度为2的数组的个数应该是(n-2)个。

        而且已知,3x3是8个元素组成,4x4是12个元素组成,5x5是16个元素组成,可以推出,当是nxn的时候,该布局由4(n-1)个元素组成。

        也就是说当用户输入由4(n-1)个元素组成的大数组a,想要n*n的多宫格布局的时候,需要将该数组a头尾分别切除n个元素组成新数组,然后将a数组剩下的元素以每两个组一组新数组的方式进行递归处理,最后将这些小数组包容在一个大数组里面 :

['宇宙战将1','白起2','太阳系级宇宙战舰3','大西洋级蘸酱8','小破木船4','太阳级蘸酱7','月球级蘸酱6','地球级宇宙战将5']


[
  ['宇宙战将1','白起2','太阳系级宇宙战舰3'],
  ['大西洋级蘸酱8','小破木船4'],
  ['太阳级蘸酱7','月球级蘸酱6','地球级宇宙战将5']
]

        按照这个指导思路实现的方法如下:

// 对数组头部和尾部进行处理
// arr: 待处理数组
// level: 多宫格层级,就比如九宫格因为是3*3所以层级为3
export function spliceArray(arr,level) {
    if (Array.isArray(arr) && level>=3) {
        let list = [...arr];
        let head = list.splice(0,level);
        let tail = list.splice(list.length-level,list.length-1);
        // 将剩余数组按两个一组进行分组
        let tempArr = departArray(list,2);
        tempArr.unshift(head);
        tempArr.push(tail);
        return tempArr
    }
}

// 将输入数组两两成组,输出包含这些小数组的大数组
function departArray(arr,length,store){
    let list = [...arr];
    let temList = store || [];
    if(list.length>=length){
        temList.push(list.splice(0,length))
        return departArray(list, length, temList);
    }else {
        return temList;
    }
}

         实现效果:

         然后我们再加入数据补全功能,比如使用者输入了四个数组但是却想要3x3八个元素的九宫格布局,那么就补足四个元素凑够八个元素来进行生成:

        

        然后再加入对于高亮的样式的传参,默认元素的样式的传参,以及 补足元素的传参(该参数未传就使用默认设置,即“谢谢惠顾”),总体代码如下:

<template>
  <div class="main">
    <div
      class="row"
      v-for="(item,index) in cellList"
      :key="index"
      :style="{height:`${100/level-1}%`}"
    >
      <div
        class="cell"
        v-for="val in item"
        :key="val.id"
        :style="val.id === activeCell ? {...highLightStyle,width:`${100/level-1}%`} : {...defaultStyle,width:`${100/level-1}%`}"
      >{{val.name}}</div>
    </div>
    <div class="startAndCancel" @click="handleStart">{{ timer ? '结束' : '开始' }}</div>
  </div>
</template>
  
<script>
import { judgeKeys, spliceArray, nineGridOrder } from "./util.js";
export default {
  name: "CellsComponents",
  props: {
    cellConfig: {
      type: Object,
      require: true
    },
    cellData: {
      type: Array,
      require: true,
      default: () => []
    }
  },
  data() {
    return {
      level: 3,
      presents: [],
      presentsOfMap: {},
      defaultId: "",
      interval: 500,
      noPrizeDes: "谢谢惠顾",
      highLightStyle: {
        color: "#fff",
        backgroundColor: "orange"
      },
      defaultStyle: {},
      activeOrder: 0,
      timer: null
    };
  },
  mounted() {},
  components: {},
  computed: {
    cellList() {
      return spliceArray(this.presents, this.level);
    },
    activeCell() {
      return this.presentsOfMap[this.activeOrder].id;
    },
    cellCount() {
      return (this.level - 1) * 4;
    }
  },
  methods: {
    handleStart() {
      if (!this.timer) {
        this.aroundFunction(this.interval + 1, true);
      } else {
        clearTimeout(this.timer);
        this.aroundFunction(50, false);
      }
    },
    aroundFunction(time, type) {
      if (time === 500) {
        if (this.defaultId && this.defaultId !== "") {
          if (this.defaultId === this.activeCell) return this.aroundStop();
        } else {
          return this.aroundStop();
        }
      }
      this.timer = setTimeout(() => {
        let ms = type ? time - 30 : time + 30;
        if (this.activeOrder < this.cellCount - 1) {
          this.activeOrder = this.activeOrder + 1;
        } else {
          this.activeOrder = 0;
        }
        if (ms < 50) {
          ms = 50;
        }
        if (ms > 500) {
          ms = 500;
        }
        this.aroundFunction(ms, type);
      }, time);
    },
    aroundStop() {
      clearTimeout(this.timer);
      this.timer = null;
      return null;
    },
    reformPresents(arr) {
      let list = [...arr];
      for (let i = list.length; i < this.cellCount; i++) {
        list.push({
          id: "thanks" + i,
          name: this.noPrizeDes
        });
      }
      const { maps, data } = nineGridOrder(list, this.level);
      // 生成关于待选项的Map结构数据
      this.presentsOfMap = maps;
      this.presents = data;
    }
  },
  watch: {
    cellConfig: {
      handler(newV) {
        const newConfig = judgeKeys(newV);
        if (newConfig) {
          Object.assign(this, newConfig);
          this.activeOrder = 0;
          this.reformPresents(this.cellData);
        }
      },
      deep: true,
      immediate: true
    },
    cellData: {
      handler(newV) {
        if (!newV || !Array.isArray(newV) || newV.length < 1) {
          throw "请输入至少一个奖品";
        }
        this.activeOrder = 0;
        this.reformPresents(newV);
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

<style lang="less" scoped>
.main {
  width: 100%;
  height: 100%;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  .startAndCancel {
    width: 33%;
    height: 33%;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 20;
    background-color: aqua;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .row {
    width: 100%;
    height: 24%;
    display: flex;
    justify-content: space-between;
    .cell {
      width: 24%;
      display: flex;
      justify-content: center;
      align-items: center;
      border: 1px solid orange;
      color: orange;
    }
  }
}
</style>

       util.js:

/**
 *  function, 判断对象内是不是有无意义的键值对,有的话进行消除
 *  @param obj Object, 待判断的对象
 *  @return Object,处理完毕的对象
 * */
export function judgeKeys(obj) {
    let newObj = {};
    for (let key in obj) {
        if (obj[key] !== undefined && obj[key] !== "" && JSON.stringify(obj[key])!=='{}') {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}


/**
 *  function, 将顺序从Z型布局变成圈型布局
 *  @param arr Array, 待改变顺序的数组
 *  @param level Number, 所希望生成的多宫格的层级
 *  @return {
 *     maps: Object,改变后数组的Map结构,用于快速查找对应元素
 *     data: Array,改变后的数组
 *  }
 * */
export function gridOrder(arr,level){
    if(Array.isArray(arr) && arr.length==(4*level-4)){
        let maps = {};
        // 对数据生成map结构并输出,方便定位和查找
        let list = arr.map((item,index)=>{
            maps[index] = item;
            return item
        });
        // 在当前层级下期望的元素总个数
        const expectCount = 4*level - 4;
        // 取要进行对应插入的尾部
        let tailArr = list.splice(expectCount-level+2,expectCount).reverse();
        // 取要进行翻转处理的尾部
        let otherTail = list.splice(expectCount-2*level+2,expectCount).reverse();
        // 进行对应插入
        for (let i = 0; i < tailArr.length; i++) {
            list.splice(level+2*i, 0, tailArr[i])
        }
        // 重新组合后输出
        const data = list.concat(otherTail)
        return {
            maps,
            data,
        }
    }else{
        throw('层级与数据对应不正确,请检查输入层级后重试')
    }
}

/**
 *  function, 对数组进行分组处理,比如8个元素的数组会被分成3+2+3
 *  @param arr Array, 待处理的数组
 *  @param level Number, 所希望生成的多宫格的层级
 *  @return Array,改变后的数组
 * */
export function spliceArray(arr,level) {
    if (Array.isArray(arr) && level>=3) {
        let list = [...arr];
        let head = list.splice(0,level);
        let tail = list.splice(list.length-level,list.length-1);
        let tempArr = departArray(list,2);
        tempArr.unshift(head);
        tempArr.push(tail);
        return tempArr
    }
}

// 将数组按两个一组进行分组
function departArray(arr,length,store){
    let list = [...arr];
    let temList = store || [];
    if(list.length>=length){
        temList.push(list.splice(0,length))
        return departArray(list, length, temList);
    }else {
        return temList;
    }
}

        父组件调用:

<template>
  <div class="cell">
    <Cells :cellConfig="cellConfig" :cellData="presents"/>
    <button @click="cellConfig.level=3">3X3</button>
    <button @click="cellConfig.level=4">4X4</button>
    <button @click="cellConfig.level=5">5X5</button>
    <button @click="cellConfig.level=6">6X6</button>
    <button @click="cellConfig.level=7">7X7</button>
    <button @click="cellConfig.level=8">8X8</button>
  </div>
</template>

<script>
import Cells from "@/components/Cells/index.vue";
export default {
  name: "CellPage",
  components: {
    Cells
  },
  data() {
    return {
        presents: [
          {
            name: "宇宙战将1",
            id: "001"
          },
          {
            name: "白起2",
            id: "002"
          },
          {
            name: "太阳系级宇宙战舰3",
            id: "003"
          },
          {
            name: "小破木船4",
            id: "004"
          },
          {
            name: "地球级宇宙战将5",
            id: "005"
          },
          {
            name: "月球级蘸酱6",
            id: "006"
          },
          {
            name: "太阳级蘸酱7",
            id: "007"
          },
          {
            name: "大西洋级蘸酱8",
            id: "008"
          }
        ],
      cellConfig: {
        level:3,
        defaultId: "",
        interval: 500,
        noPrizeDes: "",
        highLightStyle:{},
        defaultStyle:{}
      }
    };
  }
};
</script>

<style scoped>
.cell {
  width: 500px;
  height: 500px;
}
</style>

         实现效果:

 参考文章链接(排名不分先后):

  1. 九宫格抽奖简单实现_l-ddui的博客-CSDN博客_九宫格抽奖
  2.  js实现九宫格抽奖_南初️的博客-CSDN博客_js九宫格抽奖 
  3. 前端js实现九宫格模式抽奖(多宫格抽奖) - 南之骄阳 - 博客园
  4. 前端js实现九宫格模式抽奖(多宫格抽奖)_anban7417的博客-CSDN博客
Logo

前往低代码交流专区

更多推荐