☰
年会抽奖不求人:纯HTML+JavaScript实现滚动抽奖页面
2026/10/7 5:27:52 网站建设 项目流程

简介:这是一份基于HTML/CSS/JS构建的年会抽奖页面源码,主要面向前端开发者、企业行政及活动策划人员,可在年会、庆典或团队活动中快速部署使用。页面采用JSON格式集中管理参与者名单,并预留Ajax接口以动态加载后端数据,支持一键开始/暂停抽奖、背景音乐开关及多套视觉主题切换,现场交互效果比较震撼。压缩包共29个文件,含1个HTML入口页面、8个JS脚本(覆盖抽奖逻辑、滚动动画、数据读取与兼容处理)、3个CSS样式文件(含Bootstrap基础样式和自定义主题)、15张图片素材以及1首MP3背景音乐,整体体积仅3.58MB,目录划分简洁,便于直接修改或二次集成。该资源目前已有5123人学习浏览,适用于希望快速上手并理解前端抽奖动画实现的人群;解压后可自行替换名单、音乐与图片,同时通过阅读JS和CSS结构,可学习随机抽取、动画调度、本地数据配置等常见前端技巧。

1. 年会抽奖别用PPT,一个 HTML 文件才是正经解法

每年年底,行政和技术支持最怕被问“年会抽奖怎么做”。我见过有人花一周开发小程序,结果现场连不上网;有人用 PowerPoint 现场临时按名字,被员工吐槽“手速就是内幕”。其实一套好看的 html 年会抽奖页面,把名字导进去就能跑,前端源码拆开改改字体、配色、特效,半小时就能交付。这份资源就是典型的纯前端抽奖页面,单页 HTML 加原生 CSS 和 JavaScript,不依赖任何框架、不需要后端,双击 index.html 就能在投影仪上开抽。适合行政、开发、以及被临时拉去救场的运维同学,你只需要会改中括号里的字符串。

2. 抽奖页面的技术选型:三种玩法对比与核心抽奖逻辑

2.1 滚动抽奖、转盘抽奖、随机亮名,到底该选哪个

先别急着打开源码,搞清楚三种常见玩法的适用场景,你才知道这份资源值不值得直接用。滚动抽奖是屏幕上名字快速跳动,按停止后慢慢定住,视觉效果最热闹,适合大屏幕、百人以上的主会场;转盘抽奖适合奖品少、人数少的部门聚餐,指针转动有仪式感,但人一多转盘上放不下名字;随机亮名则是直接显示一个结果,没有过程感,适合抽单项大奖时穿插。这份资源走的是滚动抽奖路线,因为它对人数规模最宽容,名单再多也只是滚动列表的长度问题,不会像转盘那样挤成一团。

从实现成本上看,转盘需要绘制扇形、计算角度和落点,随机亮名最简单但缺乏仪式感,而滚动抽奖只需要一个定时器来回切换数组下标,前端基础足够就能改得动。如果你年会有二三百人,我强烈建议就用滚动抽奖;如果只有不到二十人,转盘更出效果,但那份源码里的转盘版本通常需要引入 Canvas,改动量不是一个量级。这个资源的主文件就是针对滚动抽奖场景做的,逻辑集中在一个 JS 文件里,没有花哨的框架依赖,这对年会前三天才拿到需求的你来说,就是最大的安全感。

2.2 公平性的核心:先随机后动画,而不是边转边决定

很多新手写抽奖页面时会把动画和结果混在一起:每 50 毫秒换一个名字,停止时最后显示的那个名字就是中奖人。这听起来没问题,但如果你让定时器直接跑,停止的那一瞬间到底选中谁,取决于浏览器事件的排队情况,不同电脑上表现会不一致。更严谨的做法是“先抽好结果,再播放动画”——点击开始时,先用随机数生成最终中奖人的索引,然后让滚动效果只是在最终结果上反复“绕圈”,最后落到那个已经确定的索引上。这样不管动画怎么跳,结果都是预先算好的,不会出现最后一下卡顿导致显示错误。

这份源码里的逻辑基本就是上面说的做法:一个selectWinner()函数负责从人员数组里随机取下标,一个startRoll()负责启动定时器滚动展示,定时器里每次取“当前下标”而不是“随机下标”,等滚动时间到,再切换到最终中奖下标。这种设计的好处是抽奖过程可回放、可审查,即使有人拿手机录屏,也能说明白“动画停下时正好就是随机结果”。如果你后续想加入去掉已中奖人员,只需要在selectWinner()里过滤数组即可,在第三章我会给出具体改法。

// 核心逻辑示意:先定结果,再滚动画 let employees = ["张伟","李娜","王强","赵敏","刘洋"]; // 完整名单由后台导出 let rollingTimer = null; let finalIndex = -1; function selectWinner() { if (employees.length === 0) return -1; return Math.floor(Math.random() * employees.length); // 先锁定中奖下标 } function startRoll() { finalIndex = selectWinner(); // 抽奖结果在这里已经固定 let current = 0; rollingTimer = setInterval(() => { // 定时器只负责“转”,不负责“选” document.getElementById("display").textContent = employees[current]; current = (current + 1) % employees.length; }, 50); // 50ms 切换一次名字 }

上面代码说明了先选后动的顺序:finalIndex在startRoll一开始就生成,而定时器里current只是顺序递增,等停止时把display定位到employees[finalIndex]即可。这里的 50 毫秒是滚动速度,太快会有残影,太慢显得拖沓,我一般建议 30 到 80 毫秒之间。如果你发现现场大屏刷新率不高,60ms 更稳妥。

2.3 为什么不用后端存储,纯前端反而更适合年会

有人会问,不用后端怎么保证名单安全?说实话,年会场合适用的技术方案是“越简单越可靠”。后端方案意味着现场要有服务器、要有网络、要有数据库账号,哪个环节临时出错都会变成灾难。纯前端页面把名单写死在 JS 数组里,或者从同目录的 JSON 文件读取,启动即用,断网也不怕。只要在抽奖开始前清空浏览器缓存,就不会有人拿开发者工具改名单——真正的会场里,行政拿笔记本连投影仪,你把源码文件拷到桌面,用 Chrome 打开就是全部操作。

这份资源走的就是这条路。它把人员数据集中在一个data.js或app.js的数组里,你可能只需要打开源码搜employees或names就能找到名单位置。后续你在改的时候,最容易翻车的就是编码问题——如果文件是 UTF-8 却用了 GBK 打开再保存,中文名字会变成乱码,这点我在第四章会专门展开。先记住一个习惯:用 VS Code 打开,右下角显示 UTF-8 再保存,别用 Windows 自带的记事本碰它。

3. 把源码改成自己公司的版本:名单、速度、特效一次说透

3.1 先看清源码文件结构,别一上来就改 HTML

拿到这份 html 源码后,第一步不是急着改名字,而是先看一眼目录结构。常见的组织方式是一个index.html文件把结构、样式、逻辑全部内联写在一起,也有拆成css/和js/子目录的。如果全在单文件里,你只需要用编辑器打开那一个文件;如果是拆分结构,就要保证index.html引用的路径是对的,否则直接双击会白屏。先按 F12 打开控制台,看有没有红色报错,没有报错再开始改。

我个人建议你不要去动 HTML 的骨架,改样式和脚本就足够。HTML 里的<div id="display">是显示名字的区域,<button id="startBtn">开始按钮,<button id="stopBtn">停止按钮,这些 id 在脚本里都有对应引用。你要做的是先跑通默认案例,确认可以正常抽奖,再往名单数组里填自己的员工。这样出了画面上的问题,你知道是自己改坏了哪块,而不是一来就陷入关联混乱。

3.2 把名单换成你们公司的:数组、去重与编码

找到脚本里的名单数组,常见变量名是employees、names或list。把它替换成你们同事的名字,注意保留英文逗号和引号,否则脚本会直接报错。从 Excel 导出名字时,我一般先在 Excel 里拼好“张伟”“李娜”这种带引号的格式,再整段粘贴到编辑器里,避免手动打引号出错。

// 名单数组:每行一个名字,去掉真实员工,这里只做演示 let employees = [ "张伟", "李娜", "王强", "赵敏", "刘洋", "陈静", "杨帆", "周涛", "吴磊", "许晴" ];

上面这个数组就是整个抽奖系统的数据源,你只需要把中文名字替换成你们团队的人名。注意两点:第一,名字之间必须是英文逗号,最后一个名字后面可以不加逗号;第二,重复名字一定要先去重,否则同一个李娜会出现两次,被抽中两次都不知道怎么回事。Excel 里去重用“数据 → 删除重复项”就行,复制进来之前先做这一步,能省掉现场最大的尴尬。

如果你需要从 CSV 导入,可以用下面的方式在浏览器里离线转换,然后复制到源码里。

// 在浏览器控制台跑这段,假设 clipboard 里是逗号分隔的名单 const csvText = "张伟,李娜,王强,赵敏,刘洋"; const names = csvText.split(",").map((n) => n.trim()).filter(Boolean); console.log(JSON.stringify(names)); // 把输出拷回源码数组

这里先split按逗号切分,再trim去掉首尾空格,最后filter(Boolean)过滤空行。年会开始前临时加人、减人,用这个思路改数组最省事。注意:如果你的人名里本身有逗号,比如“王,大锤”,这种建议先把 CSV 里的逗号换成全角逗号,避免被误切。

3.3 调速:滚动时长、切换间隔和停止后的特效

抽奖的节奏感非常影响现场气氛。滚动太快看不清,太慢又像卡顿;停止时机太快没有悬念,太慢又让人着急。这份源码里通常有interval和duration两个参数,interval控制名字切换速度,duration控制从开始到停止的总时长。我一般把interval设在 60ms,duration设在 3000ms,也就是三秒后自动停。你还可以根据大屏尺寸调整名字字号,字号越大,切换速度越要慢一点,否则人眼跟不上。

// 调参区域:界面右侧或脚本顶部 const config = { rollInterval: 60, // 名字切换间隔,单位 ms,推荐 30-80 rollDuration: 3000, // 滚动总时长,单位 ms,推荐 2000-5000 fontSize: "64px", // 中奖名字字号,大屏建议 56-72px freezeAfterStop: true // 停止后是否锁定结果并加高亮动画 };

上面参数中,rollInterval每隔 60ms 刷新一次显示名字,rollDuration到 3000ms 时会自动执行停止逻辑。如果你希望手动点停止,而不是自动停,就把rollDuration设成 0,然后绑定停止按钮的点击事件。freezeAfterStop为true时,停止后会触发一个高亮动画,比如背景闪烁或弹出“恭喜”遮罩,这个要看源码里有没有写对应的类名。

3.4 改皮肤:背景、喜庆配色与中奖音效

好看的页面主要靠 CSS,这恰恰是很多人不敢下手的部分。但这里的套路很简单:找到:root或body里的 CSS 变量,换成你们公司的主题色。年会场合适用红色、金色、深蓝,我个人偏爱深色背景加金色大字,因为投影仪在亮环境下的对比度更好。

:root { --page-bg: #1a0f0f; /* 深酒红,减少投影反光 */ --text-color: #ffd700; /* 金色,年会氛围感强 */ --accent-glow: 0 0 30px rgba(255, 215, 0, 0.6); /* 发光特效 */ --button-bg: #c0392b; --button-radius: 12px; }

改 CSS 变量时注意,字体发光效果text-shadow是现场最容易出彩但也是最容易翻车的点。发光范围过大,名字反而看不清,我一般控制在 20px 左右。如果你想换背景图片,找到body的background属性,替换成一张本地图片的相对路径即可,注意别用网络链接,现场断网就会露馅。

3.5 音效与浏览器自动播放的兼容处理

很多抽奖页面会配一段紧张的音效,停止时再放一段掌声。但这个坑特别常见:桌面浏览器会阻止没有用户交互的自动播放,你点击开始按钮时触发的声音没问题,但停止时的音效如果是在定时器回调里触发的,可能被浏览器拦截。解决方法是提前预加载音频,并在点击事件里先调用一次play(),让浏览器认为后续播放是用户手势的一部分。具体做法如下。

// 预加载音效,避免自动播放被拦截 const audio = new Audio("./sound/success.mp3"); document.getElementById("startBtn").addEventListener("click", () => { audio.load(); // 预加载资源 // 这里可以播放一段“开始音效” }); document.getElementById("stopBtn").addEventListener("click", () => { audio.play().catch(() => { // 如果被拦截,至少不影响功能,只做降级 console.warn("音效被浏览器拦截,忽略一下"); }); });

这里必须加.catch(),否则浏览器拦截时,未处理的 Promise 报错会显示在控制台,现场又有人传“程序崩了”。降级只是少了一段音频,抽奖结果不受任何影响。

4. 现场最容易翻车的 5 个坑:乱码、卡顿、中了又中怎么解

4.1 名字变成乱码

现象:打开页面后,本来好好的员工名单在投影仪上变成“????”或一堆火星文。

原因:95%的可能是源码文件用 GBK 编码保存,而 HTML 声明的是 UTF-8,两个编码不一致。另一个常见原因是 Excel 粘贴时带了不可见字符。

解决:用 VS Code 打开 JS 或 HTML 文件,点击右下角编码按钮,选择“通过编码保存”,改为 UTF-8 后重新保存。如果是从 Excel 直接复制,先在记事本里转一道,过滤掉格式信息再粘贴进去。我把这条放在第一条,因为乱码一旦出现在大屏上,行政会疯掉。

4.2 点击“开始”按钮页面白屏

现象:点开始后名字没有滚动,整个区域变成白色,控制台报一堆错。

原因:最常见的错误是数组为空,或者脚本里有语法错误。比如你删名单时把最后一行多删了一个逗号,整个employees数组定义失效。

解决:按 F12 打开 Console,看第一条报错的行号。如果是Unexpected token,基本是括号或逗号问题,回到数组定义处仔细检查。另外,确认按钮绑定的函数名称和源码里写的完全一致,我见过有人把startRoll改成startRoll(),反而把函数引用变成立即执行,导致初始化失败。

4.3 名单滚动时一卡一卡,大屏像幻灯片

现象:名字切换频率明显低于预期,有时 200ms 才跳一次,伴随画面闪烁。

原因:多半是页面渲染开销过大,比如背景图片太大、CSS 阴影过多、或者名字区域使用了filter: blur()这类高性能消耗属性。另一个原因是你开了太多浏览器扩展,影响 JS 执行。

解决:先用默认配置跑一遍,如果默认流畅,说明你改的 CSS 有问题,逐个取消阴影、模糊、动画属性,最先取消filter相关属性。如果默认就卡,把interval从 60ms 调到 80ms 或 100ms,降低切换频率后肉眼几乎分辨不出来,但流畅度会显著改善。

4.4 抽中了同一个同事两次

现象:第一次抽中的“李娜”,第二局又出现在中奖名单上,全场哄笑。

原因:没有在抽奖前把已中奖人员从employees数组里移除,或者移除后没有重新计算employees.length。

解决:在selectWinner被调用前,先执行一次过滤,把上一轮中奖的人从数组里摘掉。最简单的方法是用splice,在中奖结果展示后立即执行。

let winners = []; // 存储已中奖人 function markWinner(index) { winners.push(employees[index]); employees.splice(index, 1); // 从名单里移除,下一次不会抽到 renderWinners(winners); // 更新屏幕左侧的中奖记录 }

上面的splice会改变原数组,所以startRoll里要始终读取最新的employees。注意:如果你的源码已经在每轮结束后原样复活了名单,记得把复活逻辑禁用,否则系统会认为所有员工都一直可抽。

4.5 关闭浏览器刷新后,中奖记录全没了

现象:抽到一半误触了 F5,或者笔记本休眠唤醒后页面重置,中奖记录回到零。

原因:页面所有状态都保存在内存变量里,没有持久化。这对年会现场是致命的,所以我在第五章给你一个保存到localStorage的方案。

解决:临时救场时,一边用笔记录中奖名单,一边把页面里的中奖人同步更新到一张表格里。但更稳妥的是直接用下面的进阶方案,刷新后恢复所有状态。

5. 让抽奖更有“专业感”的技巧:把中奖记录写进浏览器存储

5.1 用 localStorage 实现刷新不丢,离线也能恢复现场

浏览器自带localStorage,可以把键值对存到本地磁盘,就算刷新、关机、重启浏览器,数据还在。年会开始前,在代码初始化时读取一次,抽中一个人就追加一条,同时渲染到页面的“已中奖名单”区域。这样就算现场有人不小心碰了 F5,你也能从上次中断的位置继续,而不是两眼一抹黑。

// 中奖记录持久化 const STORAGE_KEY = "annual-party-winners-2025"; function loadWinners() { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : []; } function saveWinners(winners) { localStorage.setItem(STORAGE_KEY, JSON.stringify(winners)); } function addWinner(name) { const winners = loadWinners(); winners.push(name); saveWinners(winners); renderWinners(winners); }

这段代码把中奖名单序列化成 JSON 字符串存进浏览器。loadWinners在页面加载时执行,把上一次的数组还原;addWinner每抽中一人就追加并立即保存;renderWinners负责把数组显示在页面上。这里的关键是不要手动去改localStorage里的内容,万一格式坏了,直接localStorage.removeItem(STORAGE_KEY)重置即可。

5.2 彩排时用“测试名单”验证,别拿真实名单反复抽

还有一个小技巧:在data.js里把名单数组抽成一个变量,现场前先换成testEmployees,里面放着“测试1、测试2、测试3”这种明显不是真人的名字。彩排通过后,再把真实名单换回来。这样既能完整走一遍流程,又不会在彩排时把真实员工抽出来导致尴尬。如果你心急,直接在控制台执行employees = ["测试1","测试2","测试3"]也能临时覆盖,但刷新后会还原,适合快速验证。

从那以后我每次年会前都强制走一遍流程:先复制一份index.html作为备份,在副本里把名单换成测试数据,验证数组长度、按钮事件、音效和 localStorage 记录全部正常后,再把真实名单覆盖回去,最后用无痕窗口冷启动一次,确认没有任何缓存残留。这个习惯帮我避免过一次现场空名单白屏的惨剧,希望你用不到,但值得保留。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询