ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

纯前端HTML年会抽奖源码:名字滚动、随机抽签与公平性实现

纯前端HTML年会抽奖源码:名字滚动、随机抽签与公平性实现 简介这是一份面向年会组织者和前端开发者的HTML抽奖互动源码通过名字滚动与闪烁效果实现现场抽奖。每个奖项独立展示对应奖品未抽奖时界面显示抽奖按钮点击后员工信息持续滚动闪烁停止时提示中奖用户已抽奖的奖项则直接展示中奖者信息历史中奖名单也可一并呈现。资源共18个文件压缩包96.45MB核心文件包括两个HTML页面、配套CSS样式、jQuery脚本以及多张PNG/JPG界面素材另附MP4效果演示和readme说明目录结构清晰下载解压即可运行或二次修改。目前已有3408人学习下载适合需要快速落地年会抽奖场景、缺少前端方案或希望定制奖项交互的读者。通过源码可掌握名字循环滚动、闪烁高亮、随机停止等常见抽奖动画实现思路同时获得一套可直接部署的完整界面与配套素材。1. 名字滚动年会抽奖一个HTML文件就能跑起来的现场互动每年年会行政最头疼的环节之一就是抽奖。线上抽奖工具看着专业真要现场用常常被网络、账号、插件折腾得够呛。与其赌平台的稳定性不如在本地放一个HTML文件双击打开名字高速滚动再点一下幸运儿定格——这就是名字滚动年会抽奖最朴素也最稳的实现方式。整份源码就是纯前端的一个网页没有框架、没有后端、没有构建步骤适合行政同学拿去直接改也适合前端新手用来练手和拆解。2. 滚动抽奖的核心原理定时器、随机索引与刷新方式怎么选2.1 名字“滚”起来的本质用定时器高频切换显示文本名字滚动从视觉上看像动画实际上做起来比想象中简单它不是一个平滑的位移动画而是“显示某个名字→极短时间后再显示另一个名字”的高速切换。人眼在切换频率超过每秒十几帧的时候就基本分辨不出单次切换只看到一个名字在“滚”。在浏览器里实现这种高频切换最常见的手段是setInterval或者setTimeout链。每 50 毫秒换一次名一秒钟就是 20 次视觉上已经很连续了。第一次写这个功能时我最初的直觉是用innerHTML去拼接一堆 HTML 标签其实完全没必要纯文本场景下textContent更快、更安全还能避免名字里带尖括号时把页面搞坏。// 假设名单长这样 const names [张伟, 李娜, 王强, 刘洋, 陈静, 杨帆] // 每 50 毫秒换一个名字 let timer setInterval(() { const idx Math.floor(Math.random() * names.length) document.getElementById(display).textContent names[idx] }, 50)这段代码里Math.floor(Math.random() * names.length)会得到 0 到names.length - 1之间的整数相当于随机挑一个下标然后取出对应的名字。setInterval的回调每 50 毫秒执行一次就形成了滚动。50 毫秒这个值不是拍脑袋定的实际体验里 50 到 80 毫秒比较舒服再慢就明显看到名字一格格跳再快容易给人眩晕感现场大屏尤其明显。这里有个隐藏问题直接Math.random()从整个名单里挑这套逻辑用到正式抽奖时会导致已经中过奖的人再次被抽到。滚动展示阶段随便点没关系但最终的结果判定必须做去重这个留给 2.2 讲。2.2 抽奖公平性从哪来随机索引与去重逻辑抽奖最关键的一条是公平。把生成随机数的代码直接拿来用只能保证“每次随机”不能保证“每个人都被抽过一轮后才开始下一轮”。现场最常见的尴尬是抽完三等奖二等奖又抽到同一个人——运气好是双方都笑运气差会有人当场质疑。解决思路很简单维护一个“已中奖”数组每次抽之前先把名单过滤掉这批人或者更直接把还没中奖的人单独放一个池子里从中随机取。const all [张伟, 李娜, 王强, 刘洋, 陈静, 杨帆] const winners [] function pickWinner() { const pool all.filter(name !winners.includes(name)) if (pool.length 0) { alert(所有人都中过奖了) return null } const idx Math.floor(Math.random() * pool.length) const winner pool[idx] winners.push(winner) return winner }这个filter每抽一次就要遍历一遍全名单人数在几百以内完全够用。如果到了几千人的规模可以用一个Set来存已中奖的人判断时从 O(n) 变成 O(1)。这里我一般直接用数组理由很朴素年会名单再大也就几百上千数组的性能开销可以忽略但代码读起来最直观行政同事想改也好改。去重之外还有一重考虑要不要把“滚动过程”和“中奖人判定”分开。我的做法是滚动期间纯随机展示取悦眼睛停止时再从池子里真正抽一个保证公平。这个区分是很多写得拥挤的源码最容易搞混的地方——滚动时把结果直接加到中奖数组里然后才发现前面滚动的名字其实只是动画压根没存下来。2.3 三种渲染方式的取舍textContent、innerHTML 与 CSS 过渡确定了滚动频率和随机逻辑下一个问题是每次切换名字时用哪种方式把新名字画到屏幕上。最常见的三种写法textContent直接改文本节点安全、快不会触发 HTML 解析。innerHTML能改文本也能插标签但对“纯文本”场景是浪费而且如果名字里出现、会被当成标签解析轻则样式破掉重则页面报错。先改opacity或transform等 CSS 过渡跑完再换文本视觉柔和一些但要处理定时器和过渡回调的配合偶尔会出现“名字还没换完动画就停了”的尴尬。// 推荐的写法用 textContent 改文本配合 CSS 的过渡做透明度淡入淡出 const display document.getElementById(display) const nextIdx Math.floor(Math.random() * names.length) // 先隐藏、再换名、再显示 display.style.opacity 0 setTimeout(() { display.textContent names[nextIdx] display.style.opacity 1 }, 30)这里把opacity从 1 变成 0再变回 1如果 CSS 里写了transition: opacity 0.08s就能让每次换名之间有一点淡入淡出的感觉视觉上比硬切柔和。代价是代码多了一个setTimeout而且 30 毫秒这个延迟需要和 CSS 的过渡时间匹配。如果过渡写得太长名字切换会明显变慢写得太短又看不出效果。我的选择是最小可用版本直接用textContent硬切把滚动的频率调快一点视觉接受度已经很高。淡入淡出属于加分项放在第 6 章再讲怎么优雅地加。注意textContent和innerHTML在性能上也有差别。innerHTML每次赋值都会经过 HTML 解析器哪怕内容里没有标签也要走一遍流程textContent直接改文本节点开销更低。频繁切换时这个差距会被放大。3. 手写一个可运行的抽奖页面HTML/CSS/JS 源码与参数拆解这一章给出一份能直接保存成.html文件、双击就能跑的完整源码也是典型的 HTML 网页制作场景结构、表现、行为都在一个文件里。代码按 HTML/CSS/JS 三个部分展开每段都是完整文件里的实际内容组合起来就能跑。3.1 HTML 骨架显示区、按钮和状态提示先看结构部分。页面上需要三样东西最显眼的“当前名字”大盒子、控制开始和停止的按钮、以及一个告诉操作者“当前是滚动中还是已出结果”的状态文字。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title年会名字滚动抽奖/title style /* 在此粘贴 3.2 的 CSS */ /style /head body div classcontainer h1 classtitle年会抽奖/h1 div classdisplay-box iddisplay点击开始/div p classstatus idstatus共 6 人参与已有 0 人中奖/p div classcontrols button idbtnStart classbtn btn-primary开始/button button idbtnReset classbtn btn-secondary重置/button /div /div script // 在此粘贴 3.3 的 JavaScript /script /body /html这里id是 JS 操作的入口display是名字展示区status是动态变化的提示文字两个按钮分别绑定开始/停止和重置。把按钮设计成这样是为了现场一个按钮就能完成“开始→停止”两步不需要操作者反应重置按钮则放在旁边作为后悔药防止抽错以后只能干瞪眼。3.2 CSS 大屏适配居中布局与字号策略年会抽奖八成要投到大屏上布局上最省心的是让整个页面居中再用视口单位控制字号。桌面开发习惯用px但大屏上分辨率差异很大投影仪 1024×768 和会议室电视 3840×2160 都可能出现用px就得准备两套方案麻烦。* { box-sizing: border-box; margin: 0; padding: 0; } body { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); font-family: Microsoft YaHei, PingFang SC, sans-serif; } .container { text-align: center; width: 80vw; } .display-box { min-height: 24vh; display: flex; align-items: center; justify-content: center; font-size: clamp(48px, 10vh, 160px); font-weight: bold; color: #f5c542; background: rgba(255, 255, 255, 0.06); border: 2px solid rgba(255, 255, 255, 0.25); border-radius: 24px; padding: 4vh 4vw; transition: opacity 0.08s ease; }几个关键参数说明min-height: 24vh显示区高度按视口高度的 24% 取保证屏幕上下都有足够留白名字不会贴到边。font-size: clamp(48px, 10vh, 160px)字号随视口高度变化但限制在 48px 到 160px 之间。10vh 的意思是取视口高度的 10% 作为文字大小投影仪和电视都能自适应。background用半透明白色叠加深色渐变既突出文字又不会让白底黑字在灯光下刺眼。transition: opacity 0.08s ease这个和 2.3 说的淡入淡出对应0.08 秒是 80 毫秒比滚动间隔稍长一点点视觉上比较舒服。按钮的样式这里也一并给出注意.btn的公共样式和两个按钮的颜色区分.btn { min-width: 160px; margin: 0 12px; padding: 14px 32px; font-size: 28px; border: none; border-radius: 12px; cursor: pointer; color: #fff; transition: transform 0.1s ease; } .btn:active { transform: scale(0.95); } .btn-primary { background: #e94560; } .btn-secondary { background: #533483; } .btn:disabled { opacity: 0.5; cursor: not-allowed; }transform: scale(0.95)是按下时把按钮缩小 5%松开恢复现场用投影仪时这个细微反馈很重要操作者能从画面上确认“我真的按下去了”。禁用态:disabled用于防止抽奖过程中重复点击——这个问题在 5.5 会展开讲。3.3 JavaScript 控制流开始、停止、结果判定与重置控制逻辑是整份源码的心脏。我把它拆成四个状态空闲、滚动中、已出结果、已重置。用rolling这个布尔值记状态避免用户狂点按钮导致定时器叠加。const names [张伟, 李娜, 王强, 刘洋, 陈静, 杨帆] const display document.getElementById(display) const statusEl document.getElementById(status) const btnStart document.getElementById(btnStart) const btnReset document.getElementById(btnReset) let timer null let rolling false let currentName const winners [] function updateStatus() { statusEl.textContent 共 ${names.length} 人参与已有 ${winners.length} 人中奖 } function rollOnce() { const idx Math.floor(Math.random() * names.length) currentName names[idx] display.textContent currentName } function startRoll() { if (rolling) return rolling true btnStart.textContent 停止 timer setInterval(rollOnce, 60) } function stopRoll() { if (!rolling) return clearInterval(timer) timer null // 从未中奖池子里抽一个 const pool names.filter(name !winners.includes(name)) if (pool.length 0) { alert(所有人都中过奖了) resetGame() return } const winner pool[Math.floor(Math.random() * pool.length)] winners.push(winner) display.textContent winner rolling false btnStart.textContent 开始 updateStatus() } function resetGame() { clearInterval(timer) timer null rolling false winners.length 0 display.textContent 点击开始 btnStart.textContent 开始 updateStatus() } btnStart.addEventListener(click, () { if (rolling) stopRoll() else startRoll() }) btnReset.addEventListener(click, resetGame)逻辑说明开始按钮的点击事件里做了一个三目判断——滚动中点击就停止空闲时点击就开始。这个交互参考了主流抽奖页面的习惯一个大按钮负责“开始/停止”减少现场操作者第二步思考。stopRoll里无论如何都先把setInterval清掉再用过滤出的pool随机取人确保最终被定格的人一定不是已中奖的。winners.length 0这样重置数组是故意的它比winners []更省事因为winners声明为const重新赋值会直接报错。参数说明这里的核心调参点有三个——60是滚动间隔毫秒数names是名单数组winners数组的长度决定状态提示里的“已有 X 人中奖”。如果年会分三等奖、二等奖、一等奖多轮抽奖可以不做重置让winners一直累积每轮只抽剩下的人这就是 4.3 要说的内容。4. 把名单换成自己公司的数据接入与现场微调4.1 从硬编码数组到文本框粘贴临时加减人怎么处理第 3 章的源码里名单写死在names这个数组里。实际年会现场名单的变动往往发生在最后一天甚至开场前半小时有人请假了、领导临时带了个嘉宾、新入职的同事漏报了。这时候再回头改 JS 数组虽然也能改但对不熟悉代码的人来说打开文件看到一堆const names [...]容易看懵。一个常见的做法是在页面里放一个textarea让操作者可以把名单整段粘进去用换行或者逗号分隔点一下按钮就能加载新名单。这样做还有一个好处名单可以直接从 Excel 里整列复制出来不用手动加引号和逗号。textarea idnameInput rows4 placeholder每行一个名字例如#10;张伟#10;李娜#10;王强/textarea button idbtnLoad加载名单/buttonconst btnLoad document.getElementById(btnLoad) const nameInput document.getElementById(nameInput) btnLoad.addEventListener(click, () { resetGame() // 加载前先停掉滚动状态 const raw nameInput.value.trim() if (!raw) { alert(名单不能为空) return } const newNames raw.split(/\n|,|/).map(s s.trim()).filter(Boolean) if (newNames.length 2) { alert(请至少输入两个名字) return } names.length 0 names.push(...newNames) winners.length 0 display.textContent 点击开始 btnStart.textContent 开始 updateStatus() })这段代码做了两件关键的事把文本框里的内容按换行、英文逗号或中文逗号拆开去掉首尾空格和空行然后原地更新names数组。names.length 0清空后用push(...newNames)把新名单填进去整个names的引用没有变所以前面 2.2 里的filter也不需要改。一个很容易忽略的细节是加载前先调用resetGame()。如果加载时正处于滚动中不重置就开始加载会产生一个已经“滚了一半”的定时器旧名单和新名单混在一起现场很难马上看出来。把resetGame()放在点击事件第一行这个边界问题就消掉了。4.2 调参指南滚动速度、显示字号、中奖人数一个项目能给别人用的前提是参数可调。第 3 章的完整源码里散落着几个敏感数字把它们集中到脚本顶部后续维护会省心很多。// ☆ 现场可调参数区 ☆ const CONFIG { interval: 60 // 滚动间隔(毫秒)越小滚动越快建议 40~100 }把interval从 60 毫秒改成 40 毫秒名字切换会明显更“躁”年会现场更嗨改成 100 毫秒以上就是缓和版。字号在 3.2 已经用clamp(48px, 10vh, 160px)处理了高度小于 480px 的屏幕会稳定在 48px大于 1600px 的屏幕也不会超出 160px。这两个值一般不需要再动真觉得字不够大把10vh改成12vh即可。中奖人数这个参数我的建议是不做成年会抽奖“一次抽多个”的复杂逻辑。现场更常见的需求是一轮抽一个人连续抽多轮所以winners数组不断累积反而是最自然的设计。如果你确实遇到“一次抽 5 个人”的需求最稳的写法是从池子里抽 5 个不重复的然后一次性渲染到屏幕上function pickMultiple(count) { const pool names.filter(name !winners.includes(name)) if (pool.length count) { alert(剩余人数不足) return [] } const picked [] const temp pool.slice() for (let i 0; i count; i) { const idx Math.floor(Math.random() * temp.length) picked.push(temp[idx]) temp.splice(idx, 1) } return picked }这是典型的“不放回抽样”先把池子复制一份每抽一个人就把它从临时数组里删掉保证同一次抽奖内不会重复。抽完后统一把picked里的名字加到winners里不要抽一个加一个否则状态会乱。4.3 多轮抽奖与已中奖隔离一个数组状态是怎么维护的多轮抽奖听起来复杂其实只要维护好两个数组的状态就够一个是全员名单names一个是已中奖winners。每次抽奖只从“不在 winners 里的那些人”中间选中奖后把名字推进winners。这样不用做“把中奖人从原始名单删除”的操作重置时把winners.length清空就行名单始终是原样只是查询时做过滤。这个方案的取舍在于如果年会要抽 400 人里的 100 个奖每轮都做一次filter每轮 O(n) 的遍历绝大多数浏览器都能扛住用户无感。如果确实到了几千人的规模可以把winners换成Setconst winnersSet new Set() function stopRollWithSet() { const pool names.filter(name !winnersSet.has(name)) // 其余逻辑与 stopRoll 一样 }但这里我不建议一上来就用Set因为行政同学可能还要在浏览器控制台里看winners数组、核对是不是漏了人数组打印出来比Set直观得多。等到真有性能压力再改成Set也不迟。另外一个容易被忽略的小细节重置按钮的语义。年会里“重置”不只是清空中奖名单还要把状态文字、按钮文字和显示区都恢复原样。重置按钮放在开始按钮旁边是为了给人一个后悔药——抽错人了点重置一切从头来。这也是我在 3.1 特意保留两个按钮而不是只留一个大按钮的原因。5. 年会现场不要翻车5 个高频坑与排查办法这一章写的是真实现场最容易踩的坑每一条我都按“现象→原因→解决”的方式讲方便出问题时按图索骥。5.1 现象名字滚动时明显掉帧大屏上一卡一卡的现象是名字切换不连贯有撕裂感甚至整个页面无响应。原因一般有两个一是电脑太老播放 4K 视频都费劲还要跑浏览器二是滚动间隔设得太短比如 20 毫秒加上 CSS 过渡里的opacity和阴影渲染压力变大。还有就是后台开着直播软件CPU 被打满。解决的办法先把CONFIG.interval从 40 提到 80视觉影响不大但负载明显下降然后去掉display-box的阴影纯色背景最省资源最后关闭其他占用高的程序。如果还卡换一台电脑比任何优化都立竿见影——年会现场电脑性能比前端优化更关键。5.2 现象停止后抽到已经中过奖的人这几乎是新手源码翻车率最高的一条。原因在于停止时用的是滚动过程中的currentName而不是“从剩余人里重新抽”。我在第 2 章就强调了滚动展示和结果判定必须分开这里再补一个排查办法打开浏览器控制台输入winners看数组里到底有没有存结果。如果winners是空的说明停止逻辑根本没走数据更新问题在stopRoll里。解决也是这套把stopRoll里“先清定时器、再 filter 抽人、最后写回 display”的顺序固定死。清定时器永远在第一步不要等结果算完再清否则这 60 毫秒里定时器还会偷跑一次把画面换到别的名字上。5.3 现象中文名显示成乱码或问号现象是页面上名字变成“锟斤拷”或者一串问号。原因几乎都是编码问题HTML 文件保存时没有用 UTF-8或者meta标签和文件实际编码不一致。解决用 VS Code 或 Notepad 打开文件另存为时选择 UTF-8 编码确认头部有meta charsetUTF-8。这里有个细节有些 Windows 老版本记事本会把文件存成 GBK再把meta写成 UTF-8就会出乱码。最好的办法是统一用现代编辑器保存不要用系统自带记事本。5.4 现象双击 HTML 文件打不开或者打开后按钮点了没反应打不开的原因常见有两种文件被系统当成默认用记事本打开了——右键选择“打开方式→浏览器”即可或者是浏览器安全策略拦了本地脚本这种情况多见于某些国产浏览器的“极速模式”。按钮点了没反应先按 F12 打开控制台看有没有红色的报错信息。最常见的报错是Cannot read properties of null意思是某个getElementById没找到元素。原因多半是 HTML 里id写错了或者script放在了body前面导致脚本执行时元素还不存在。解决办法很简单把script放到/body之前或者用DOMContentLoaded包一层。5.5 现象按钮连点导致逻辑错乱抽奖结果和画面不一致现场操作者紧张时会快速连点开始/停止以前的代码如果只用timer是否为空来判断状态就会出现第一次点击启了定时器第二次点击清掉第三次点击又启一个但此时另一个定时器还没清干净两个定时器同时跑结果越来越乱。解决用独立的rolling布尔值做闸门startRoll里if (rolling) returnstopRoll里if (!rolling) return并且两个函数入口先把timer清干净。这是 3.3 源码里已经写过的方式。另外按钮的disabled也可以在滚动期间禁用不过年会现场禁用按钮反而会让操作者困惑“怎么点了没反应”所以我更偏向用逻辑闸门而不是禁用按钮。6. 增强体验的两个小技巧与公平性验证6.1 用控制台快速验证随机分布写完后怎么确认这个抽奖没有黑幕最简单的方法是用控制台跑一个分布测试。把names换成 100 个数字模拟抽 10 万次看每个名字被抽到的次数是否接近均匀。const testNames Array.from({ length: 100 }, (_, i) P${i}) const counter new Array(100).fill(0) for (let i 0; i 100000; i) { const idx Math.floor(Math.random() * testNames.length) counter[idx] } console.log(最低次数:, Math.min(...counter)) console.log(最高次数:, Math.max(...counter))如果最低和最高之间的差异在 5% 以内说明Math.random()的分布基本可靠。这个验证每次做完抽奖页我都会跑一遍能排除自己写错随机索引的可能。6.2 给滚动加一个“减速停”的小技巧年会现场最有仪式感的瞬间是主持人喊“停”之后看着名字慢慢定下来。默认的setInterval一停就是硬停没有“减速”的感觉。一个低成本的做法停止时把setInterval换成链式setTimeout让延迟逐次变长模拟减速。function stopRollWithDecel() { clearInterval(timer) let delay 80 const decelerate () { delay 70 if (delay 700) { finishPick() // 复用 3.3 里的抽奖判定逻辑 return } rollOnce() setTimeout(decelerate, delay) } setTimeout(decelerate, delay) }核心思路是把固定 60 毫秒的间隔改为动态递增第一次触发在 80 毫秒后第二次 150 毫秒第三次 220 毫秒……名字切换就从“高频跳动”慢慢变成“一格格滑动”最后停在某个人身上。真正的获奖者还是在finishPick里走pool过滤后抽取画面定格的人只承担视觉结果。去年用这个方式做过年会效果比硬停热闹很多。但也要提醒减速过程太长会冷场从 80 毫秒加到 600 毫秒左右就行总时长控制在 4 秒内。说到底名字滚动年会抽奖这个 HTML 源码核心就这么点事一个定时器、一个随机数、一个数组去重。技术难度不高真正的价值在于把它做到“现场可用”而不是“演示可用”。我的习惯是发布前一天一定用活动的真实名单在目标电脑上跑一遍按下停止确认名字能正常定格再把名单临时改成两个人测试重复抽奖会不会触发“所有人都中过奖”的提示。这两个小动作比任何代码审查都更能保证年会当天不出状况。希望这篇实操记录能帮到你下次年会别被这个环节难住。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表