2026/10/8 21:13:43

基于JavaScript定时器的年会名字滚动抽奖:纯前端HTML源码解析

基于JavaScript定时器的年会名字滚动抽奖:纯前端HTML源码解析 简介HTML实现名字滚动年会抽奖源码包面向企业年会及活动抽奖场景适合前端开发者或行政人员快速部署。页面为每个奖项独立展示奖品未开奖时显示抽奖按钮点击后员工姓名滚动闪烁停止即提示中奖者已开奖奖项自动展示中奖用户信息流程直观。资源共18个文件以2个HTML页面、1个CSS、1个jQuery脚本为主配有多张PNG界面图、JPG背景图及演示mp4、说明txt整体96.45MB可直接运行并按需改样式。截图覆盖抽奖界面、中奖用户界面、特等奖界面等关键状态视频演示便于对照操作。目前已有3408人学习下载是落地年会抽奖功能的现成方案。1. 年会抽奖页面前先搞懂“名字滚动”到底在滚什么很多行政、策划或者临时被抓壮丁的工程师接到“html实现名字滚动年会抽奖”这个需求时第一反应是去搜现成源码下载下来改改名字就交差。结果真到了年会现场要么名字不滚动、要么抽出来的人是上一个已中奖者、要么点击按钮后页面直接白屏——现场几百双眼睛盯着连刷新后悔药都没得吃。这个标题其实点破了三个关键问题第一它是纯前端方案不依赖服务器用 HTML CSS JavaScript 三个文件或者合成一个 .html 文件就能跑第二“名字滚动”本质上是抽奖的视觉效果背后需要一套可控的定时器和随机逻辑不是单纯 CSS 动画第三年会场景对稳定性要求极高不能有网络依赖、不能卡顿、不能重复中奖。这篇文章从零把源码思路讲透从核心机制到可抄作业的完整代码再到参数调优和现场避坑最后聊怎么往公司已有的系统上衔接照着做当天下午就能出一个能上台的方案。2. 名字滚动和抽奖逻辑先定规则再写界面2.1 滚动效果的本质定时器驱动不是 CSS 动画“名字滚动”这四个字容易让人误会成 CSS 的 marquee 标签或者 animation 动画。但真正用在年会抽奖里滚动必须满足两个条件一是快快到看起来眼花缭乱二是可控点击“停止”时能立刻停在某个随机位置。marquee 标签是浏览器废弃的滚动行为animation 动画只能做视觉循环都没法在任意时刻被抽奖逻辑“打断并取值”。所以核心机制一定是 JavaScript 的定时器配合一个不断变化的随机索引每次刷新把一个名字渲染到屏幕上。视觉上字在闪动、在跳动其实就是高频重绘不是视觉上的位移。常见做法是这样把所有待抽奖名单放进一个数组用 setInterval 每 50~80 毫秒更新一次显示的姓名间隔越短滚动切换越快点击停止按钮时先 clearInterval 停在当前名字上再判断这个人是否已经在“已中奖池子”里。注意“判断是否重复中奖”必须在停止动作之后的处理函数里做这样能保留现场的真实感——有时候抽到重复人员屏幕上那个名字已经停住了再偷偷换掉反而容易露馅。2.2 两种抽奖规则放回抽和不放回抽年会抽奖一般分两种场景。一种是“所有名单反复滚动谁抽到算谁”这种叫放回抽中奖之后名字还在池子里下一轮还可能再被抽到适合做暖场小游戏中奖人可以重复上台领多次小奖品。另一种是正儿八经的奖项轮次抽奖抽走的人就不该再出现在后续轮次里这叫不放回抽中奖名单要持久地从抽奖池中剔除同时最好留一份“已中奖”存档便于主持人宣读。不放回抽的实现有两个关键点一是用 splice 从数组中删除中奖者这样后续轮次的滚动数组里自然不会有这个人二是要保留一份完整原始名单因为现场可能发生“临时加一轮”或者“主持人说上一轮不作数”的翻车情况没有原始名单就全乱了。我一般会把原始名单存一个副本中奖名单单独存一个数组每次停止后往中奖名单 push 一次既方便大屏幕轮播展示已中奖者也方便活动结束后导出核对。2.3 名单从哪来硬编码与批量粘贴两种方式新手最常犯的错是把 100 个人名一个个写在代码里。如果名单上有几百号人硬编码就非常难受改一个名字得重新打开源文件搜索替换。做得稍微像样一点的源码应该提供一个批量粘贴入口比如页面里放一个 textarea让工作人员在彩排时把 Excel 里的名字一列复制进来点“载入名单”按钮页面自动按换行或者逗号分拆成数组。这样现场改名单、补漏人、去掉请假的人都只需要复制粘贴不用碰代码。另一个实用细节是用“人数”做校验。载入名单后页面顶部显示“当前名单 128 人”如果有重复名字也提示出来比如“张三出现了两次”让工作人员当场确认。这个校验非常值得做因为 Excel 导入的名字经常有行尾空格、全角空格、重复项不在前端裁掉滚动时就会出现“两个一模一样的人”或者“名字看起来没事但匹配不上”的玄学问题。3. 可抄作业的名字滚动抽奖 HTML完整源码与参数解读3.1 写一个能直接在浏览器打开的抽奖页面源码下面这份代码是一个完整可用的纯前端抽奖页把所有 HTML、CSS、JavaScript 放在同一个.html文件里保存后用浏览器打开就能跑。它包含名单批量载入、开始滚动、停止抽奖、中奖记录、一键回滚把上一个中奖者放回去和名单查重逻辑算是比较贴近年会场地的版本。!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title年会名字滚动抽奖/title style /* 大屏优先文字和按钮都放大方便投影仪显示 */ body { background: #1a0530; color: #fff; font-family: Microsoft YaHei, PingFang SC, sans-serif; display: flex; min-height: 100vh; margin: 0; align-items: center; justify-content: center; } .wrapper { text-align: center; background: rgba(255, 255, 255, 0.06); border-radius: 24px; padding: 48px 56px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45); width: 90%; max-width: 960px; } #displayName { font-size: 120px; font-weight: 900; letter-spacing: 8px; margin: 32px 0 48px; min-height: 160px; line-height: 160px; background: linear-gradient(135deg, #fada5e, #ff8c00); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .btn { padding: 16px 40px; font-size: 22px; border: none; border-radius: 40px; background: #fada5e; color: #1a0530; font-weight: bold; margin: 0 12px; cursor: pointer; transition: opacity .2s ease; } .btn:hover { opacity: .85; } .btn-secondary { background: rgba(255, 255, 255, 0.15); color: #fff; } #listArea { width: 100%; margin: 24px 0 12px; } textarea { width: 100%; height: 110px; font-size: 16px; padding: 12px; border-radius: 8px; border: none; background: #fff; color: #222; resize: vertical; } .meta { font-size: 18px; margin-bottom: 24px; color: #ddd; } .history { margin-top: 28px; font-size: 18px; color: #fada5e; letter-spacing: 2px; } /style /head body div classwrapper h2年会名字滚动抽奖/h2 textarea idlistArea placeholder每行一个名字直接粘贴Excel中的一列名单/textarea div stylemargin: 8px 0 16px; button classbtn-secondary onclickloadList()载入名单/button button classbtn-secondary onclickundoLast()回滚上一人/button /div div classmeta idmetaInfo当前名单 0 人已中奖 0 人/div div iddisplayName准备开始/div div button classbtn idstartBtn onclickstartRoll()开始滚动/button button classbtn btn-secondary onclickstopRoll()停止抽奖/button /div div classhistory idhistoryText中奖名单/div /div script // 全局状态 let list []; // 当前抽奖池不放回抽奖时中奖的人会从这里剔除 let fullList []; // 原始完整名单用于回滚和核对 let winners []; // 已中奖名单用于展示和去重 let rolling false; // 是否正在滚动 let timer null; // 定时器句柄 // 名单载入 function loadList() { const raw document.getElementById(listArea).value; // 按换行、逗号、顿号做分隔 const arr raw.split(/[\n,、]/).map(s s.trim()).filter(Boolean); // 查重重复的名字给出提示但依然保留避免现场尴尬 const seen new Set(); const dup new Set(); arr.forEach(n { if (seen.has(n)) dup.add(n); seen.add(n); }); if (dup.size 0) { alert(发现重复名字 Array.from(dup).join(, ) \n请先确认是否误粘贴。); } list [...arr]; fullList [...arr]; winners []; updateMeta(); renderName(准备开始); document.getElementById(historyText).innerHTML 中奖名单; } // 滚动逻辑 function startRoll() { if (list.length 0) { alert(名单为空请先载入名单); return; } if (rolling) return; // 已经在滚了不重复启动定时器 rolling true; timer setInterval(randomPick, 60); // 60ms 换一次名字视觉上足够花 } function randomPick() { const idx Math.floor(Math.random() * list.length); renderName(list[idx]); } // 停止与中奖处理 function stopRoll() { if (!rolling) return; rolling false; clearInterval(timer); // 从池中拿走中奖者 const winner document.getElementById(displayName).textContent; const finalName winner; winners.push(finalName); // 从待抽池删除该中奖者后续轮次不会再次出现 list list.filter(name name ! finalName); // 更新界面 updateMeta(); const historyEl document.getElementById(historyText); historyEl.innerHTML 中奖名单 winners.join( / ); // 池子抽完了提示 if (list.length 0) { alert(所有参与者都已中奖可以点击“载入名单”重新开始下一场。); } } // 回滚上一人主持人口误或现场加奖时使用 function undoLast() { if (winners.length 0) return; const back winners.pop(); list.push(back); updateMeta(); renderName(back); const historyEl document.getElementById(historyText); historyEl.innerHTML winners.length ? 中奖名单 winners.join( / ) : 中奖名单; } // 工具函数 function renderName(text) { document.getElementById(displayName).textContent text; } function updateMeta() { document.getElementById(metaInfo).textContent 当前名单 list.length 人已中奖 winners.length 人; } /script /body /html这份代码的变量改起来非常容易核心都集中在script标签里。其中list是当前剩余可抽人员每次有人中奖就把它从名单中剔除fullList是载入时的全部人员如果后续要临时恢复被误删的人直接重新载入就行winners保存已中奖者顺序即中奖顺序。3.2 几个必须调好的参数滚动间隔、字体尺寸和中奖手势这条代码里最基本的参数是setInterval(randomPick, 60)里的 60 毫秒。这个值决定了名字切换频率。数值越小字闪得越快看起来越刺激但对投影仪来说如果刷新过快反而会因为人眼和投影的残影让文字糊成一团基本看不清名字气氛到了但效果打折。我试过几个值50~80 毫秒是大多数会议室投影能接受的范围如果现场用的是大型 LED 屏幕可以调到 40 毫秒让滚动更急促视觉张力更强。#displayName的font-size是另一个大屏关键参数。投影距离远、屏幕亮度一般字号建议在 96px 以上这份代码给了 120px在 1080p 的分辨率下大概能覆盖小半个屏幕。如果现场屏幕是 4K最好把字号往 160px 甚至 200px 加。注意min-height和line-height也要跟着同比例改否则字上下跳动时页面会轻微抖动给人感觉不太专业。停止后的确认动作也值得雕琢如果只靠“名字停在屏幕上”作为中奖信号后排观众不一定看得清我一般建议在停止的瞬间同时触发一个全屏闪烁动画或者弹窗展示“恭喜 XXX”因为人眼对动态变化的注意力比静态文字强得多。4. 从能用走向好用大屏适配、多条名单轮换与本地部署4.1 让滚动区域用满整个投影关键 CSS 适配策略年会现场往往只有一台 HDMI 线接笔记本的画面输出网页在浏览器里开着但浏览器地址栏、标签栏和系统任务栏都会占据一部分高度直接全屏模式显示的才是真正的“大屏视觉”建议现场按F11进入浏览器全屏。如果页面留白太多就要把.wrapper的max-width改大甚至去掉再配合body的font-size做整体缩放让内容自动适应不同分辨率。我一般会在源码里给.wrapper增加一个transform: scale()的测试按钮用于在彩排时按比例放大或缩小整个抽奖界面。这样不需要每次改 CSS、刷新页面只需要按一个“适配大屏”按钮内部用document.documentElement.style.fontSize改根字体大小配合rem单位达到整体缩放的效果。但要注意如果设备分辨率差异极大比如从 14 寸笔记本扩展到 100 寸投影放大后背景图和按钮可能会出现模糊或溢出最好在彩排时确认一个最适比例然后现场直接用那个参数不要等上场了再试。4.2 多轮次奖品管理一等奖、二等奖怎么切名单年会抽奖通常分为几轮一等奖一轮、二等奖一轮、三等奖一轮。如果每一轮都重新粘贴一份名单操作步骤多、容易手忙脚乱而且最后一轮一等奖通常在节目之后没人有时间去改配置。更好的做法是在源码里增加一个“轮次管理”的数据结构把每一轮的奖品名称和对应名单数量预设好点击切换轮次时自动载入当前轮次的可用名单。这里有两个常见做法一是每一轮都从原始全名单中抽取中奖者不进入下一轮二是按照部门或者候选人范围来抽。第一种方式把fullList作为每轮的基础轮次切换时list [...fullList]重置已中奖者从新一轮池子里剔除。第二种方式更灵活用一个对象保存多个分组例如{一等奖: [张三, 李四], ...}轮次切换直接把对应数组载入list。无论采用哪种方式我都建议把“当前轮次”的标签显示在页面上比如“正在抽取二等奖”避免主持人念错轮次变成事故。4.3 离线也能跑没有内网和服务器时的部署要点年会场地最怕网络抽风纯 HTML 文件天然不依赖后端把它直接拷到桌面或者用 U 盘带去现场最稳妥。需要注意的坑是现场电脑可能没有安装代码编辑器但一定会有浏览器。Windows 自带的记事本能改 HTML 源码改完保存为.html后缀文件即可双击就能用浏览器打开。如果修改后刷新页面没反应先检查是否保存成功、文件后缀是不是.html别用.txt后缀那是新手最常见的翻车点。但还有一类更隐蔽的隐患个别浏览器对本地文件读取有安全限制比如页面里使用fetch加载外部 JSON 名单文件时会报跨域错误或者直接不发请求。这个源码不涉及外部文件访问把名单粘贴到 textarea 即可绕开这个问题。如果要实现“读取本地名单文件”这种更好的体验需要把代码部署到本地静态文件服务器比如用 Python 的http.server在名单文件所在目录执行python -m http.server 8080浏览器访问http://localhost:8080/index.html这样fetch本地名单文件就不会被拦截。5. 年会场地的五个高频坑现象、原因与解决5.1 点击停止按钮后名字还在继续闪现象页面上“停止抽奖”点了没反应名字停不下来像是卡住了全场开始起哄。原因多半是rolling状态变量没维护好重复触发了setInterval导致多个定时器同时跑点击停止只清掉最后一个句柄前面的还在交替输出。解决方法是把rolling变量管严进入stopRoll时第一行就设rolling false并且在startRoll开头加if (rolling) return;防止重复启动。代码里已经写了这个防重复逻辑现场若还遇到检查是否改过源码时误删了clearInterval(timer)那行。5.2 中奖名单里出现了两个“张三”现象第一轮张三中奖了下一轮滚动的名单里又出现张三最后张三大包大揽中了两轮场面很尴尬。原因是在用filter删掉中奖者时如果原始名单里有两个相同字样的名字一个来自 Excel 的隐藏空格filter(name name ! finalName)会把两个都删掉但页面显示的是一个等下一次停止时如果选中了另一个同名字段显示就重复。解决方式是把中奖判断改成“依据下标删除”即随机停止时记录当前框选名字在数组中的索引用splice(index, 1)删除指定位置的元素而不是按字面值匹配删除。5.3 名单粘贴后出现一个空白的滚动项现象滚动的过程中偶尔出现瞬间的空白或者停止时停在空白上让人摸不着头脑。原因是 textarea 里的名单存在空行而分割时没有过滤掉空字符串。虽然源码里用了filter(Boolean)过滤空项但如果你把源码改过或者用了一个只看/\n/分割的简化版本就会出现空白项。解决方式是载入名字后打印调试信息在控制台输出list.length同时肉眼确认metaInfo显示的人数与 Excel 实际人数一致只要人数对不上就能立刻反推出是分隔符或空行出了问题。5.4 字体太小后排观众根本看不清名字现象散场后很多人吐槽“只看到在滚谁中奖根本不知道”。原因是对会场大小预估不足字号用 66px 甚至更小100 寸投影上只占一个角。解决思路是在彩排时站在最后一排看屏幕感觉看不清就加字号至少 120px 起同时把背景色做深、文字做亮形成高对比度。另外滚动文字的颜色最好只用一个高饱和的颜色比如金黄、亮白不要在名字上做花哨的渐变或阴影投影环境下这些质感都会糊掉。5.5 中奖名单太大屏幕上放不下被截断现象中奖记录从一行变成一行放不下滚动条出现或者文字换行挤占了滚动区域。原因是historyText直接拼接长字符串没有做自动省略或分页。解决方式是把历史中奖名单控制在只显示最近 5 人例如winners.slice(-5).join( / )完整名单通过浏览器控制台查看或者用alert(JSON.stringify(winners))导出核对。现场真正重要的信息是“当前中奖者是谁”历史列表只是一个气氛辅助放太多反而干扰主视觉。6. 把源码接进公司系统微信通知、Redis 名单与后续玩法年会抽奖做完之后很多组织者会希望把名单同步到公司内部系统、发企业微信通知中奖人或者把抽奖结果留档到后台。纯 HTML 页面要做到这些常见做法是给页面增加一层数据导出功能把winners数组通过浏览器下载成 JSON 或 CSV 文件再给管理员使用。导出这步不需要后端用 Blob 生成文件就能完成代码如下function exportWinners() { const data winners.map((name, i) ${i 1},${name}).join(\n); const blob new Blob([序号,姓名\n data], { type: text/csv;charsetutf-8; }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download winners.csv; a.click(); URL.revokeObjectURL(url); }上面这段导出代码把中奖名单存成 CSV 文件用 Excel 打开不会乱码因为第一行输出了 UTF-8 的 BOM 头。要注意文件名winners.csv不要加时间戳之前先确认现场电脑的时间正确否则排序错乱很难排查。如果公司内部用企业微信想自动发通知就需要一个后端接口了可以把winners数组 POST 到内部 API例如用fetch(/api/winners, { method: POST, body: JSON.stringify(winners) })提交权限和鉴权由后端统一处理前端这里只需要考虑接口调通之后触发一次页面提示。回到这个标题本身用 HTML 做名字滚动抽奖真正的价值不在于技术难度而在于把一场活动最需要用到的交互逻辑用最轻量、最耐折腾的方式承载起来。我见过太多同事为了做一个抽奖页专门去搭 Vue 工程、装依赖包结果到了现场 Node 环境出问题、构建产物没拷全最后不得不退回 PowerPoint 动画。如果你只是要一个能在 20 分钟内配置好、不依赖网络、现场坏了能立刻改源代码重开的工具那么这种单 HTML 文件的方案永远不会辜负你。我的习惯是每次年会前在会议室彩排三遍一遍正常抽、一遍连续抽完所有人、一遍故意点错停止再回滚重抽三遍都顺了才带着 U 盘去现场希望这份源码和这些经验帮到你。本文还有配套的精品资源点击获取