Vue3实现九宫格抽奖功能

前言

 好久没有写文章了,上一次发文还是年终总结,眨眼间又是一年,每每想多总结却是坚持不来,难顶。
 这次分享一个九宫格抽奖小游戏,缘起是最近公司内部做积分抽奖需求,抽出其中抽奖动效做一个总结,从零实现一个抽奖功能的过程和注意点。
 Demo功能代码是使用vue3实现的,主要用到了组合式API,由于只是一个简单的demo就没有使用脚手架和打包工具。

需求与效果

需求:
1、礼品根据后台配置生成
2、跑马灯转动效果
3、结果后台生成并且每个礼物概率不一样(概率这里不讨论)

注意点:
1、布局如何排列,是按照跑动排列还是从左至右自上而下排列
2、点击按钮如何插入,DOM结构如何生成
3、跑马效果如何实现,速度如何控制
4、接口如何处理,包括接口报错、请求Pending时特效
5、后台返回结果和跑马完选中结果一致等

最终效果图:

GIF 2022-11-16 九宫格抽奖.gif

注:图片都是百度图片找的

功能实现

第一步:实现布局

思路:
 请求到后台配置的礼物列表,一般是配置了8个礼物,包括谢谢参与,第一种是手动布局写9个礼物div标签,这种方式开始按钮可以直接写到布局里面。第二种可以遍历,这里我使用遍历的方式,但是按钮就需要我们插入到礼物列表数组中去。
 css代码可以使用flex布局,三行三列刚好,然后添加所需样式。js部分主要是使用splice()方法插入<开始按钮>。

部分代码:

<body>
    <div id="app" v-cloak>
        <div class="container">
            <div :class="['item', {'active': currentIndex === index}]"
                v-for="(item, index) in prizeList"
                @click="start(index)">
                <img :src="item.pic" alt="">
                <p v-if="index !== 4">{{ item.name }}</p>
            </div>
        </div>
    </div>
</body>
const state = reactive({
  prizeList: [
    { name: '手机', pic: 'https://bkimg.cdn.bcebos.com/pic/3801213fb80e7bec54e7d237ad7eae389b504ec23d9e' },
    { name: '手表', pic: 'https://img1.baidu.com/it/u=2631716577,1296460670&fm=253&fmt=auto&app=120&f=JPEG' },
    { name: '苹果', pic: 'https://img2.baidu.com/it/u=2611478896,137965957&fm=253&fmt=auto&app=138&f=JPEG' },
    { name: '棒棒糖', pic: 'https://img2.baidu.com/it/u=576980037,1655121105&fm=253&fmt=auto&app=138&f=PNG' },
    { name: '娃娃', pic: 'https://img2.baidu.com/it/u=4075390137,3967712457&fm=253&fmt=auto&app=138&f=PNG' },
    { name: '木马', pic: 'https://img1.baidu.com/it/u=2434318933,2727681086&fm=253&fmt=auto&app=120&f=JPEG' },
    { name: '德芙', pic: 'https://img0.baidu.com/it/u=1378564582,2397555841&fm=253&fmt=auto&app=120&f=JPEG' },
    { name: '玫瑰', pic: 'https://img1.baidu.com/it/u=1125656938,422247900&fm=253&fmt=auto&app=120&f=JPEG' }
  ], // 后台配置的奖品数据
})
const startBtn = { name: '开始按钮', pic: 'https://img2.baidu.com/it/u=1497996119,382735686&fm=253' }

onMounted(() => {
    // state.prizeList.forEach((item, index) => {
    //  item.id = index
    // })
    state.prizeList.splice(4, 0, startBtn)
    console.log(state.prizeList)
})
第二步:实现动效

思路
 跑马灯效果的实现主要是转圈高亮,这里我们可以想到当前礼物坐标和跑马执行步数除以8的余数一致时就高亮,但是跟礼物列表渲染的顺序又不一样,所以我们可以定义一个跑马执行的礼物坐标顺序数组 prizeSort = [0, 1, 2, 5, 8, 7, 6, 3],这样就是转圈执行啦。
 然后动画一步一步的执行的话我们可以使用一个定时器,然后隔一点时间让当前高亮的下标索引currentIndex变化
 转动的圈数我们可以自定义几圈,这里我们用总执行步数计算,必须是8的倍数,比如每次要转4圈,那基本的总执行步数就是32步,再根据后台中奖的礼物坐标计算出还要走多少步加上32即跑马执行的总部数
 转动速度的话一般是先快后忙,我们利用定时器setTimeout()方法的话第二个参数的等待时间就要越来越长,可以判断如果执行总步数超过一般或三分之二就开始增加定时器等待时间,让下一步执行越来越慢。

第三步:后台抽奖结果

思路:
 后台返回抽奖结果的话,这里可能出现上面注意点问题4和5,接口报错和长时间等待,我们处理方式可以为,点击先请求返回结果再开始动效,对于接口报错我们可以直接提示出来,如何长时间等待我们可以增加一个loading提示,这样就基本可以解决这两个问题了。对于后台返回结果和动效执行完结果如何匹配的问题,上面说过,我们有个基本的执行步数,然后根据请求结果的礼物坐标计算出还要走执行多少步,相加结果就可以匹配上。

代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {  margin: 0; padding: 0; box-sizing: border-box; }
        [v-cloak] {
            display: none;
        }
        .container {
            width: 450px;
            height: 450px;
            background: #98d3fc;
            border: 1px solid #98d3fc;
            margin: 100px auto;
            display: flex;
            flex-wrap: wrap;
            justify-content: space-around;
            align-items: center;
        }
        .item {
            width: 140px;
            height: 140px;
            border: 2px solid #fff;
            position: relative;
        }
        .item:nth-of-type(5) {
            cursor: pointer;
        }
        .item img {
            width: 100%;
            height: 100%;
        }
        .item p {
            width: 100%;
            height: 20px;
            background: rgba(0, 0, 0, 0.5);
            color: #fff;
            font-size: 12px;
            text-align: center;
            line-height: 20px;
            position: absolute;
            left: 0;
            bottom: 0;
        }
        .active {
            border: 2px solid red;
            box-shadow: 2px 2px 30px #fff;
        }
    </style>
</head>
<body>
    <div id="app" v-cloak>
        <div class="container">
            <div :class="['item', {'active': currentIndex === index}]"
                v-for="(item, index) in prizeList"
                @click="start(index)">
                <img :src="item.pic" alt="">
                <p v-if="index !== 4">{{ item.name }}</p>
            </div>
        </div>
    </div>

    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script>
      const { createApp, onMounted, ref, reactive, toRefs, computed } = Vue
      
      createApp({
        setup () {
            const state = reactive({
              prizeList: [
                { name: '手机', pic: 'https://bkimg.cdn.bcebos.com/pic/3801213fb80e7bec54e7d237ad7eae389b504ec23d9e' },
                { name: '手表', pic: 'https://img1.baidu.com/it/u=2631716577,1296460670&fm=253&fmt=auto&app=120&f=JPEG' },
                { name: '苹果', pic: 'https://img2.baidu.com/it/u=2611478896,137965957&fm=253&fmt=auto&app=138&f=JPEG' },
                { name: '棒棒糖', pic: 'https://img2.baidu.com/it/u=576980037,1655121105&fm=253&fmt=auto&app=138&f=PNG' },
                { name: '娃娃', pic: 'https://img2.baidu.com/it/u=4075390137,3967712457&fm=253&fmt=auto&app=138&f=PNG' },
                { name: '木马', pic: 'https://img1.baidu.com/it/u=2434318933,2727681086&fm=253&fmt=auto&app=120&f=JPEG' },
                { name: '德芙', pic: 'https://img0.baidu.com/it/u=1378564582,2397555841&fm=253&fmt=auto&app=120&f=JPEG' },
                { name: '玫瑰', pic: 'https://img1.baidu.com/it/u=1125656938,422247900&fm=253&fmt=auto&app=120&f=JPEG' }
            ], // 后台配置的奖品数据
            currentIndex: 0, // 当前位置
            isRunning: false, // 是否正在抽奖
            speed: 10, // 抽奖转动速度
            timerIns: null, // 定时器实例
            currentRunCount: 0, // 已跑次数
            totalRunCount: 32, // 总共跑动次数 8的倍数
            prizeId: 0, // 中奖id
            })
        const   startBtn = { name: '开始按钮', pic: 'https://img2.baidu.com/it/u=1497996119,382735686&fm=253' }

        // 奖品高亮顺序
        const prizeSort = [0, 1, 2, 5, 8, 7, 6, 3]

        // 要执行总步数
        const totalRunStep = computed(() => {
            return state.totalRunCount + prizeSort.indexOf(state.prizeId)
        })

        onMounted(() => {
            // state.prizeList.forEach((item, index) => {
            //  item.id = index
            // })
            state.prizeList.splice(4, 0, startBtn)
            console.log(state.prizeList)
        })

        // 获取随机数
        const getRandomNum = () => {
            // const num = Math.floor(Math.random() * 9)
            // if (num === 4) {
            //  console.log(">>>>>不能为4")
            //  return getRandomNum()
            // } else {
            //  return num          
            // }
            // 这里一次必然可以取到 时间为1次
            return prizeSort[Math.floor(Math.random() * prizeSort.length)]
        }

        const start = (i) => {
            if (i === 4 && !state.isRunning) {
                // 重置数据
                state.currentRunCount = 0
                state.speed = 100
                state.isRunning = true

                console.log('开始抽奖,后台请求中奖奖品')
                // 请求返回的奖品编号 这里使用随机数 但不能为4
                // const prizeId = getRandomNum()
                // console.log('中奖ID>>>', prizeId, state.prizeList[prizeId])
                // state.prizeId = prizeId
                // 模拟接口延时返回 如果接口突然报错如何处理?直接调用stopRun()方法停止转动
                setTimeout(() => {
                    const prizeId = getRandomNum()
                    console.log('中奖ID>>>', prizeId, state.prizeList[prizeId])
                    state.prizeId = prizeId
                }, 2000)
                startRun()
            }
        }

        const startRun = () => {
            stopRun()
            console.log(state.currentRunCount, totalRunStep.value)
            // 要执行总步数
            // 已走步数超过
            if (state.currentRunCount > totalRunStep.value) {
                state.isRunning = false
                return
            }
            state.currentIndex = prizeSort[state.currentRunCount % 8]
            // 如果当前步数超过了2/3则速度慢下来
            if (state.currentRunCount > Math.floor(state.totalRunCount * 2 / 3)) {
                state.speed = state.speed + Math.floor(state.currentRunCount / 3)
                console.log('速度>>>>', state.speed)
            }
            state.timerIns = setTimeout(() => {
                state.currentRunCount++
                startRun()
            }, state.speed)
        }

        const stopRun = () => {
            state.timerIns && clearTimeout(state.timerIns)
        }

        return {
            ...toRefs(state),
            start
        }
        }
      }).mount('#app')
    </script>
</body>
</html>

结尾

上面就是抽奖功能的基本实现,代码是使用vue3编写的小demo,也没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库,几年之前刚学前端时也写过抽奖,那是还是用jQuery写的,想了解可以看下jQuery版本


本文是笔者总结编撰,如有偏颇,欢迎留言指正,若您觉得本文对你有用,不妨点个赞~

关于作者:
GitHub
简书
掘金

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容