恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
年会大屏抽奖翻车后,我拆解了JS手机号批量滚动抽奖代码
首页
资讯中心
/
年会大屏抽奖翻车后,我拆解了JS手机号批量滚动抽奖代码
年会大屏抽奖翻车后,我拆解了JS手机号批量滚动抽奖代码
发布时间:2026/10/17 15:35:22
简介这份资源提供了一套基于JavaScript的手机号批量滚动抽奖实现代码面向需要为年会、晚会或选秀类活动制作现场抽奖环节的前端开发者与活动策划人员。核心解决随机抽取且不重复、定时器驱动滚动展示、中奖人数可控等实际问题适合具备基础JS与DOM操作能力的读者参考。资源包共1个文件为PDF格式大小约56KB内容以代码讲解与实现思路为主便于快速阅读和对照实践。目前已有368人学习下载。读者可从中获取随机索引生成、已选号码去重、setInterval滚动刷新、中奖人数控制以及与后台通信的完整实现逻辑并附有HTML界面结构示例涵盖奖项等级、中奖人数与金额设置等交互细节有助于理解抽奖程序从原理到落地的关键环节并在此基础上扩展动画效果与安全校验。1. 年会大屏抽奖翻车之后我拆了一份 js 手机号批量滚动抽奖实现代码年会前一周行政把抽奖环节甩给我说“就大屏滚手机号主持人喊停就出结果”。我第一反应是找个现成的 js 手机号批量滚动抽奖实现代码改改结果搜出来的东西要么是单号码轮盘要么是抽完不防重彩排时同一个工号连出三次台下哄笑我后背全是汗。后来拿到这份代码包通读一遍发现它把“随机 不重复 定时器滚动 人数控制”四件事串成了一条完整链路还带了奖等、金额、回车键控制这些现场细节属于能直接搬上年会大屏的那种。它适合两类人一类是前端基础一般、但需要在活动前快速交付一个可用抽奖页的开发者另一类是想搞懂“批量滚动抽奖”背后索引随机化逻辑、避免现场翻车的熟手。下面我按自己复现和改造的顺序把这份 js 手机号批量抽奖代码拆开讲重点放在参数怎么调、坑在哪、后台怎么接。2. 拆开代码包DOM 结构、样式与抽奖数据源怎么对上这份代码不是纯 JS 片段而是一个完整 HTML 页面结构上分成三层背景与奖等展示层、号码滚动列表层、底部设置层。理解这三层怎么对应是后面改人数、改号码池的前提。2.1 页面骨架与关键 class 的职责先看 DOM 主干我把和抽奖逻辑强相关的节点标出来div classprize_con p classprize_gradespan开始抽奖/span i0/i元/p div classprize_list vetically ul li000000/li !-- 共 10 个占位 li -- /ul /div p classstartimg srcimages/prize_start.png alt/p ul classprize_set li classset_grade奖等 select idset_grade option选择奖等/option option特等奖/option option一等奖/option option二等奖/option option三等奖/option /select /li li classset_people人数input typetel placeholder输入中奖人数 idprizeCount/li li classset_money金额input typetel placeholder输入中奖金额 idprizeMoney/li /ul /div input typehidden value0 idprize_btn.prize_list ul是滚动号码的容器初始 10 个li只是占位真正显示什么由showRandomNum决定。.start是开始/停止按钮它内部img的src会在prize_start.png和prize_stop.png之间切换这是现场主持人视觉反馈的关键。#prize_btn这个隐藏 input 是状态锁0表示未开始、1表示滚动中所有点击逻辑都先读它。#set_grade、#prizeCount、#prizeMoney三个控件分别控制奖等文案、单次中奖人数、奖金额。样式里有两个点值得注意。.prize_list设了width:55%; height:350px; overflow:hidden意味着号码列表区域是固定高度裁切的如果你把单次人数从 10 改成 20li会换行但可能被裁掉需要同步调高度或字号。.prize_list li的width:20%配合font-size:0的父级ul是经典的 inline-block 五列布局10 个号码正好两行。改人数时这个宽度百分比要跟着算否则会挤成一行或错位。2.2 号码池 code 数组与索引随机化代码里号码池是一个硬编码数组var code [302610,210022,159862,158602,145635,856997,586223,546221,145213,987451, 251364,854136,581698,123785,521387,752169,718954,412321,898989,121245, 788565,458558,589659,455212,964632,458412,223344,112233,335566,778899];这里用的是 6 位邀请码不是真实手机号但原理完全一样。核心函数showRandomNum(num)做了一件很聪明的事它不直接对code数组洗牌而是先生成一个和code等长的索引数组arr对arr做随机排序再取前num个索引去映射code。function showRandomNum(num) { var li ; for (var i 0; i code.length; i) { arr[i] i; } arr.sort(function () { return 0.5 - Math.random(); }); for (var i 0; i num; i) { var index arr[i]; li li code[index] /li; } $(.prize_list ul).html(li); }逻辑说明arr每次调用都重新填充为[0,1,2,...,code.length-1]然后sort传入一个返回0.5 - Math.random()的比较函数。这个写法在中小规模数组上能打乱顺序取前num个索引天然不重复因为索引本身来自同一个不重复序列。参数num就是当前要展示的号码个数由#prizeCount的输入值传入。这里有个隐含边界如果num大于code.lengtharr[i]会越界得到undefined页面会出现空白li所以人数上限必须小于号码池长度。提示arr.sort这种随机比较函数在超大数组比如几万条上分布并不均匀且性能一般。年会级别几百到几千个号码完全够用但如果号码池上万建议换成 Fisher-Yates 洗牌后面第 5 章会给替换写法。2.3 定时器滚动与开始/停止状态机点击.start后的逻辑是一个典型的状态机靠#prize_btn的值切换$(.start).click(function () { if ($(#prize_btn).val() 0) { // 校验奖等、人数、金额 if ($(#set_grade).val() 选择奖等) { alert(请选择奖等); return; } else if ($(#prizeCount).val() ) { alert(请输入中奖人数); return; } else if ($(#prizeCount).val() 10) { alert(单次抽奖人数不能超过10人); return; } else if ($(#prizeMoney).val() ) { alert(请输入中奖金额); return; } else { $(#prize_btn).val(1); var num $(#prizeCount).val(); $(this).find(img).attr(src, images/prize_stop.png); myNumber setInterval(function () { showRandomNum(num); }, 30); } } else { $(#prize_btn).val(0); clearInterval(myNumber); $(this).find(img).attr(src, images/prize_start.png); } });逻辑说明第一次点击进入if分支做四项校验全部通过后把状态锁置1按钮图换成“停止”启动setInterval每 30 毫秒调一次showRandomNum(num)形成号码快速滚动的视觉。第二次点击进入else分支状态锁归0clearInterval停掉定时器按钮图换回“开始”。参数30是滚动帧间隔单位毫秒数值越小滚动越快、CPU 占用越高现场大屏一般 30 到 50 都合适低于 20 在低配电脑上会卡顿。回车键控制是现场主持人的刚需代码里绑了keydown$(document).keydown(function (event) { var e event || window.event || arguments.callee.caller.arguments[0]; if (e e.keyCode 13) { $(.start).click(); } });这样主持人不用去点鼠标按回车就能开始和停止彩排时这个细节能省很多事。奖等和金额的联动也在$(function(){})里#set_grade的change事件把选中值写进.prize_grade span#prizeMoney的keyup把输入值写进.prize_grade i大屏顶部就会实时显示“特等奖 5000元”这样的文案。3. 把代码跑起来本地复现、号码池替换与后台对接拿到一份抽奖代码最怕的是“看起来能跑一换数据就崩”。这一章按我实际复现的顺序从目录结构到号码池替换再到和后台的边界划分一步步来。3.1 目录结构与本地启动这份代码依赖 jQuery页面里引的是js/jquery.min.js背景图在images/firstp_bg.jpg按钮图在images/prize_start.png和images/prize_stop.png。一个能跑的最小目录长这样draw/ ├── index.html ├── js/ │ └── jquery.min.js └── images/ ├── firstp_bg.jpg ├── prize_start.png └── prize_stop.png把index.html里的抽奖脚本原样放进去双击打开就能看到大屏效果。如果图片缺失页面不会报错只是背景和按钮空白抽奖逻辑照常运行所以彩排前一定要确认三张图都在。jQuery 版本没有硬性要求1.x 到 3.x 都能跑因为用到的只有$、click、change、keyup、html、attr、val这些基础 API。本地验证时我一般会先把setInterval的间隔从 30 改成 300让滚动变慢肉眼确认每次showRandomNum出来的号码确实不重复再改回 30 上大屏。这个“慢速验证”习惯帮我抓过好几次索引越界和重复问题。3.2 号码池替换从硬编码到接口拉取硬编码code数组只适合演示真实活动号码来自报名表或投票系统。常见做法是把号码池换成一个接口返回的数组。假设后台提供一个返回 JSON 的地址前端可以这样改var code []; function loadCodes(callback) { $.getJSON(/api/draw/codes, function (res) { // res 约定为 { data: [302610, 210022, ...] } if (res res.data res.data.length 0) { code res.data; callback callback(); } else { alert(号码池为空请检查后台数据); } }); } $(function () { loadCodes(function () { console.log(号码池加载完成共 code.length 条); }); });逻辑说明loadCodes在页面初始化时拉取号码池成功后再允许抽奖。参数方面接口返回的data必须是字符串或数字数组长度至少大于单次最大中奖人数。这里有个容易忽略的点showRandomNum里arr的长度依赖code.length如果接口是异步的用户可能在号码池还没回来时就点了开始导致arr为空、页面无反应。所以要么在加载完成前禁用按钮要么在showRandomNum开头加一句if (code.length 0) return;。如果活动方坚持用真实手机号记得在展示时做脱敏比如138****8888这既是隐私要求也能避免大屏上完整号码被拍照传播。脱敏可以在showRandomNum拼li时处理function maskPhone(phone) { var s String(phone); if (s.length ! 11) return s; return s.slice(0, 3) **** s.slice(7); }然后在li li maskPhone(code[index]) /li;里调用。参数11是手机号长度邀请码场景直接返回原值即可。3.3 前端抽奖与后台去重的边界原代码注释里有一句很实在的话“去除中奖的手机号这个前端貌似没法实现只能后台去控制”。这是这份代码最值得尊重的边界意识。前端能做的是“单次展示不重复”靠的是索引数组洗牌但“跨轮次不重复”必须后台兜底因为前端刷新页面、换台电脑、甚至改内存都能绕过。常见做法是前端每次停止滚动后把本轮中奖号码 POST 给后台后台在数据库里标记这些号码已中奖下一轮拉取号码池时只返回未中奖的。前端这边可以在停止时加一个提交动作function submitWinners(winners, grade, money) { $.ajax({ url: /api/draw/commit, type: POST, contentType: application/json, data: JSON.stringify({ grade: grade, money: money, winners: winners }), success: function (res) { if (!res.ok) { alert(后台记录失败请人工核对 winners.join(,)); } }, error: function () { alert(网络异常本轮中奖号码请人工记录 winners.join(,)); } }); }逻辑说明winners是本轮showRandomNum实际展示的号码数组需要在停止时从当前li里收集。grade和money来自设置控件。参数contentType必须是application/json否则后台解析会出问题。这个提交动作是“后悔药”即使后台失败弹窗里也带着号码主持人可以人工记录不至于现场失控。注意前端永远不要承担“判断谁没中过奖”的职责。号码池的去重、已中奖标记、奖项库存全部放后台。前端只负责把号码滚得好看、停得干脆、把结果报回去。4. 现场避坑抽奖代码最容易翻车的五个地方这份代码我前后改过三版彩排和正式上场各踩过一些坑。下面五条按“现象 → 原因 → 解决”写都是现场会要命的。4.1 同一个号码连续出现现象滚动过程中同一个号码在相邻两次刷新里都出现台下有人喊“重复了”。原因arr.sort(function(){ return 0.5 - Math.random(); })这种洗牌方式在数组较小时分布不均匀而且每次showRandomNum都重新洗牌理论上相邻两次的前num个索引可能重叠。单次展示内部不重复但跨刷新会重复视觉上就像“没抽干净”。解决如果要求滚动过程中也不出现重复可以把洗牌结果缓存一轮滚动时按顺序切片展示而不是每次重洗。或者接受“滚动过程允许重复、停止那一刻不重复”因为最终结果只取停止时的li。我一般选后者并在主持人话术里说明“滚动中号码会变化以停止为准”。4.2 单次人数超过 10 导致布局错乱现象把中奖人数填成 15号码挤成三行第三行被overflow:hidden裁掉大屏上只看到 10 个。原因.prize_list高度固定 350pxli行高 100px最多容纳三行多.prize_list li宽度 20%一行五个10 个正好两行。超过 10 个就换行溢出。解决代码里已经用$(#prizeCount).val() 10做了拦截但如果你要放开上限必须同步改 CSS把.prize_list高度按Math.ceil(num/5) * 100动态设置或者缩小li的line-height和font-size。我一般会在showRandomNum里根据num动态调字号function adjustLayout(num) { var $list $(.prize_list); var rows Math.ceil(num / 5); $list.css(height, rows * 100 px); var fontSize num 10 ? 32 : 45; $list.find(li).css(font-size, fontSize px); }参数5是每行个数100是单行高度和 CSS 里的line-height保持一致。4.3 定时器没清干净停止后还在滚现象主持人按了停止号码还在跳或者按了开始没反应。原因myNumber是全局变量如果页面里有多个入口触发setInterval或者clearInterval之前又点了一次开始会叠加多个定时器。另外#prize_btn状态锁如果被其他脚本改动状态机会错乱。解决在启动新定时器前先clearInterval(myNumber)并且把myNumber置空判断if (myNumber) { clearInterval(myNumber); myNumber null; } myNumber setInterval(function () { showRandomNum(num); }, 30);停止时同样先判断再清。状态锁#prize_btn只在这一个 click 回调里读写不要在其他地方改。4.4 回车键在输入框里误触发现象主持人在“人数”输入框里按回车确认输入结果抽奖直接开始了。原因$(document).keydown绑在 document 上输入框里的回车也会冒泡到 document触发.start的 click。解决在 keydown 里判断事件源如果是 input 或 select 就跳过$(document).keydown(function (event) { var e event || window.event; var tag (e.target || e.srcElement).tagName.toLowerCase(); if (tag input || tag select || tag textarea) return; if (e.keyCode 13) { $(.start).click(); } });这样输入框里回车只用于确认输入不会误启动抽奖。4.5 号码池异步加载与点击竞态现象页面刚打开号码池还没请求回来主持人就按了开始大屏上号码全是undefined或空白。原因code初始为空数组showRandomNum里arr长度 0arr[i]越界。解决在showRandomNum开头加保护并在号码池加载完成前禁用按钮function showRandomNum(num) { if (!code || code.length 0) return; if (num code.length) num code.length; // ...原有逻辑 }同时loadCodes成功前给.start加pointer-events:none; opacity:0.5加载完成后移除。这个竞态在本地用假数据时看不出来一接真实接口就暴露属于典型的“本地好好的现场就翻车”。5. 进阶把随机索引换成 Fisher-Yates 并做一轮可验证的抽奖原代码的arr.sort洗牌在几百条号码时够用但如果你想把这份 js 手机号滚动抽奖代码用在号码池上千、且对随机均匀性有要求的场合建议把洗牌换成 Fisher-Yates。它不需要额外库几行就能替换而且结果可复现、可验证。function shuffle(arr) { var a arr.slice(); for (var i a.length - 1; i 0; i--) { var j Math.floor(Math.random() * (i 1)); var t a[i]; a[i] a[j]; a[j] t; } return a; } function showRandomNum(num) { if (!code || code.length 0) return; if (num code.length) num code.length; var indexArr []; for (var i 0; i code.length; i) indexArr.push(i); var shuffled shuffle(indexArr); var li ; for (var i 0; i num; i) { li li code[shuffled[i]] /li; } $(.prize_list ul).html(li); }逻辑说明shuffle从数组末尾往前遍历每次在[0, i]范围内随机取一个位置j交换a[i]和a[j]。这样每个元素被放到每个位置的概率相等是公认均匀的洗牌算法。参数a.length - 1是起始索引i 0保证索引 0 不用再交换。替换后showRandomNum的行为和原来一致但随机分布更稳号码池越大优势越明显。验证方法我一般用“多次抽样统计”在控制台跑 1000 次showRandomNum的索引生成逻辑统计每个索引出现的次数理想情况下频次应该接近。下面这段只用于本地验证不上生产function verifyShuffle(total, times) { var counter new Array(total).fill(0); for (var t 0; t times; t) { var idx []; for (var i 0; i total; i) idx.push(i); var s shuffle(idx); counter[s[0]]; // 只统计第一个位置 } console.log(每个索引出现在首位的次数, counter); } verifyShuffle(30, 3000);参数total是号码池长度times是模拟次数。3000 次下每个索引出现在首位的次数应该都在 100 上下浮动如果某个索引明显偏高或偏低说明洗牌有问题。这个验证不需要后台纯前端就能跑适合上线前在本地过一遍。还有一个现场技巧把每轮中奖号码在停止时同步写进localStorage作为前端侧的“黑匣子”。万一后台提交失败、网络断了刷新页面后还能从localStorage里把已出号码捞出来人工核对。写法很简单function saveWinners(winners) { var key draw_winners; var history JSON.parse(localStorage.getItem(key) || []); history.push({ time: Date.now(), winners: winners }); localStorage.setItem(key, JSON.stringify(history)); }参数key是存储键名history是数组每次追加一条带时间戳的记录。注意localStorage只适合单机兜底不能替代后台活动结束后记得清理避免下一场活动读到旧数据。从那以后我每次接年会抽奖都强制走一遍“慢速验证不重复 → 异步号码池竞态检查 → 回车键输入框过滤 → 后台提交兜底”这四步少一步心里都不踏实。希望帮到你。本文还有配套的精品资源点击获取