☰
年会抽奖HTML源码:不重复抽取与现场避坑指南
2026/9/29 18:26:37 网站建设 项目流程

简介:这是一套面向公司年会、团建活动场景的前端抽奖源码,基于HTML、CSS与JavaScript实现,适合具备基础前端能力、希望快速搭建现场互动环节的开发者使用。资源支持动态配置员工信息、奖品灵活调整,并实现员工仅能中奖一次的逻辑,同时内置多种震撼音效与酷炫视觉样式,可直接运行index.html使用。压缩包共20个文件,约34.12MB,包含html主页面、css样式、js脚本,以及mp4背景视频、mp3抽奖音效、png与jpg图片素材、psd设计源文件和readme说明,覆盖页面结构、交互逻辑与视听资源。目前已有6867人学习下载,读者可参考其抽奖逻辑、音效切换与样式调整方式,快速替换员工与奖品数据,用于年会现场或活动互动环节。

1. 年会抽奖页面的真实需求:不是炫技,是别在老板面前翻车

年会抽奖这个需求,几乎每年年底都会有人来问一遍。表面上看是「html 实现酷炫的公司年会抽奖」,实际落到执行层面,真正被关心的从来不是动画有多花哨,而是三件事:名单能不能一次导入几百上千人、抽出来的结果能不能当场服众、投影到大屏上字够不够大够不够亮。我见过太多现场翻车的案例——滚动动画卡成 PPT、抽到的人重复出现、老板临时说「把三等奖改成抽 8 个」结果代码里写死了 5 个,最后只能尴尬地重抽。

所以这篇不聊虚的,就围绕一份能直接跑的 html 抽奖源码,把结构、随机算法、动画性能、名单导入、现场应急这几件事讲透。它适合两类人:一类是行政或 HR 被临时抓来做个抽奖页,需要照着改就能用;另一类是前端想拿它当个练手项目,顺便搞清楚大屏场景下 DOM 动画和 canvas 动画到底怎么选。核心词 html、抽奖、源码会贯穿始终,因为这三样东西决定了你能不能在一个下午之内把它做出来并跑通。

我一般会把整个方案拆成「静态页面骨架 + 抽奖逻辑 + 名单数据 + 现场兜底」四块,其中真正容易出问题的只有后两块。下面从最小可运行版本开始,一步步加东西,每一步都能单独验证。

2. 从零搭一个能跑的抽奖页:DOM 结构、样式与随机逻辑

2.1 最小可运行骨架:一个按钮、一块滚动区、一份名单

先别急着上 canvas 和粒子特效,第一版只要能点一下开始、名字滚起来、再点一下停下来出结果,就算成功。这个版本大概 80 行代码,是后面所有花活的地基。

<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>年会抽奖</title> <style> /* 大屏场景:字号用 vw,保证投影后依然清晰 */ body { margin: 0; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #0b1020; color: #fff; font-family: "Microsoft YaHei", sans-serif; overflow: hidden; } #nameBox { font-size: 8vw; font-weight: 700; letter-spacing: 0.1em; min-height: 1.2em; text-shadow: 0 0 30px rgba(80, 160, 255, 0.8); } #startBtn { margin-top: 6vh; padding: 1.2vh 5vw; font-size: 2vw; border: none; border-radius: 999px; background: linear-gradient(135deg, #3b82f6, #8b5cf6); color: #fff; cursor: pointer; } #startBtn:disabled { opacity: 0.5; cursor: not-allowed; } </style> </head> <body> <div id="nameBox">准备开始</div> <button id="startBtn">开始抽奖</button> <script> // 名单先写死,后面会换成外部导入 const pool = ['张三', '李四', '王五', '赵六', '钱七', '孙八']; const nameBox = document.getElementById('nameBox'); const startBtn = document.getElementById('startBtn'); let timer = null; let running = false; startBtn.addEventListener('click', () => { if (!running) { // 开始滚动:每 50ms 换一个名字 running = true; startBtn.textContent = '停止'; timer = setInterval(() => { const idx = Math.floor(Math.random() * pool.length); nameBox.textContent = pool[idx]; }, 50); } else { // 停止:清掉定时器,当前显示的名字就是结果 running = false; clearInterval(timer); startBtn.textContent = '开始抽奖'; } }); </script> </body> </html>

这段代码里几个参数值得说清楚。setInterval的 50ms 是滚动速度,数值越小滚得越快,低于 30ms 人眼基本看不清,高于 100ms 会显得卡顿,我一般用 40 到 60 之间。font-size: 8vw是按视口宽度算的,1920 宽的投影上大概是 153px,后排也能看清。min-height: 1.2em是为了防止名字长短不一导致按钮跳动,这个细节不做的话,滚动时整个页面会上下抖,现场看着很廉价。

提示:这个版本用的是Math.random(),它本身没问题,但如果你要「不重复抽取」,就不能只靠它,得配合一个已抽集合,下一节会讲。

2.2 不重复抽取:用已抽集合而不是删数组

很多人第一反应是抽一个就从数组里splice掉一个,这样确实不会重复,但有个隐患:一旦现场需要「重抽」或者「把某个人放回去」,数组已经被改得面目全非,恢复起来很麻烦。更稳的做法是保留原始名单不动,用一个Set记录已经抽过的人。

const pool = ['张三', '李四', '王五', '赵六', '钱七', '孙八']; const drawn = new Set(); // 已抽中的人 function pickOne() { // 过滤掉已抽中的,剩下的才是候选 const candidates = pool.filter(name => !drawn.has(name)); if (candidates.length === 0) { return null; // 抽完了 } const idx = Math.floor(Math.random() * candidates.length); const winner = candidates[idx]; drawn.add(winner); return winner; }

逻辑说明:每次抽取前先算出「还没中奖的人」,从这个子集里随机取一个,取完加进drawn。这样原始pool始终完整,想重置只需要drawn.clear()。参数上唯一要注意的是candidates.length === 0的判断,抽完最后一轮必须给个提示,否则页面会一直显示上一个人,现场会以为卡住了。

2.3 把名单换成外部文件:一次导入几百人

写死在代码里的名单只适合演示,真实年会少则几十人多则上千人,必须支持外部导入。最省事的方案是读一个同目录下的names.js,里面导出一个数组,这样不用起服务器,双击 html 就能跑。

// names.js window.NAMES = [ '张三', '李四', '王五', '赵六', '钱七', '孙八', // ... 继续加 ];
<!-- 在抽奖脚本之前引入 --> <script src="names.js"></script> <script> const pool = window.NAMES || []; if (pool.length === 0) { document.getElementById('nameBox').textContent = '名单为空,请检查 names.js'; } </script>

为什么不用fetch读 txt 或 csv?因为fetch在file://协议下会被浏览器拦截,除非你起一个本地服务器。年会现场最怕的就是「还要装个环境」,所以names.js这种直接挂全局变量的方式最稳。名单格式就是一行一个名字,用引号包起来,逗号分隔,从 Excel 复制出来稍微处理一下就能用。

注意:如果名单里有英文名或特殊符号,记得检查引号是否配对,一个漏掉的引号会让整个names.js报错,页面直接白屏。

3. 让抽奖看起来「酷炫」:动画方案选型与性能边界

3.1 DOM 动画和 canvas 动画,年会场景该选哪个

「酷炫」这个词很虚,落到技术上其实就是滚动、缩放、发光、粒子这几类效果。DOM 方案用 CSS 的transform和opacity做动画,浏览器会走合成层,性能不差;canvas 方案把所有东西画在一张画布上,适合粒子多、元素多的场景。年会抽奖的元素数量其实很少——一个名字、几个按钮、最多加个背景,所以 DOM 方案完全够用,而且改起来直观,不用去算坐标。

我一般只在一种情况下上 canvas:背景要做几百个粒子飘动,或者名字要用自定义字体逐字渲染。除此之外,DOM + CSS 动画是性价比最高的选择。下面这个滚动效果就是在 2.1 的基础上加了缩放和模糊,视觉上会「高级」不少。

#nameBox { transition: transform 0.08s ease-out, filter 0.08s ease-out; } #nameBox.rolling { transform: scale(1.08); filter: blur(1px); }
// 滚动时加 class,停止时去掉 timer = setInterval(() => { const idx = Math.floor(Math.random() * pool.length); nameBox.textContent = pool[idx]; nameBox.classList.add('rolling'); }, 50); // 停止时 clearInterval(timer); nameBox.classList.remove('rolling');

transition的 0.08s 要略大于setInterval的间隔,否则动画还没结束就被下一次替换,看起来会抖。blur(1px)是模拟「高速运动看不清」的感觉,数值别超过 2px,否则名字糊成一片,现场会有人喊看不清。

3.2 中奖弹窗与结果记录:别让抽过的人再上来

抽奖抽到后面,最尴尬的是有人重复中奖。除了 2.2 的drawn集合,界面上也要有反馈——中奖后弹个窗,同时把结果记到一个列表里,方便最后统一展示。

function showWinner(name) { const modal = document.createElement('div'); modal.className = 'winner-modal'; modal.innerHTML = `<div class="winner-name">${name}</div>`; document.body.appendChild(modal); // 3 秒后自动关闭 setTimeout(() => modal.remove(), 3000); }
.winner-modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.7); z-index: 999; animation: pop 0.3s ease-out; } .winner-name { font-size: 10vw; font-weight: 900; color: #ffd700; text-shadow: 0 0 40px rgba(255, 215, 0, 0.9); } @keyframes pop { from { transform: scale(0.6); opacity: 0; } to { transform: scale(1); opacity: 1; } }

inset: 0是top/right/bottom/left的简写,兼容性在现代浏览器没问题。z-index: 999保证弹窗盖住所有内容。3 秒自动关闭是给现场留出欢呼的时间,太短没气氛,太长会挡住下一次抽奖。

3.3 大屏适配:1920 和 4K 投影下的字号与布局

年会现场的设备五花八门,有 1080P 投影,也有 4K 大屏,甚至有人直接用笔记本外接。用vw和vh做单位能自动适配,但要注意 4K 屏上8vw会变得巨大,可能超出屏幕。稳妥的做法是给字号加一个上限。

#nameBox { font-size: clamp(48px, 8vw, 160px); }

clamp(最小值, 理想值, 最大值)是 CSS 里做响应式字号最省心的函数,48px 保证小屏可读,160px 防止 4K 屏上溢出。按钮、弹窗的字号同理,都可以用clamp包一层。另外背景色建议用深色,投影仪在明亮环境下深色背景对比度更高,名字用白色或金色,别用红色——红色在有些投影上会偏暗,后排看不清。

提示:现场调试时按 F11 全屏,再按 Ctrl 加 + 或 - 调整缩放,比改代码快得多。

4. 现场避坑:抽奖页面最容易翻车的 5 个地方

4.1 现象:滚动动画卡顿,名字一跳一跳

原因:setInterval的间隔太短,或者每次滚动都在改textContent触发了重排。DOM 操作本身不慢,但高频改文字会让浏览器反复计算布局。

解决:把间隔控制在 40ms 以上,并且滚动时只改文字,不要同时改样式。如果还卡,把nameBox加一个will-change: transform,提前告诉浏览器这层要动。

4.2 现象:抽到的人重复出现

原因:只用了Math.random(),没有记录已抽中的人。随机函数本身不保证不重复,抽 10 次可能同一个人出现 3 次。

解决:用 2.2 的drawn集合,每次从「未中奖」的子集里抽。如果现场要求「可以重复中奖」,那就把drawn去掉,但要在规则里说清楚。

4.3 现象:名单导入后页面白屏

原因:names.js里有语法错误,比如中文引号、漏逗号、最后一行多了逗号。浏览器解析失败会直接中断脚本。

解决:打开浏览器控制台看报错行号,大概率是引号问题。建议用编辑器把names.js格式化一遍,或者用 JSON 校验工具过一下。

4.4 现象:点「停止」后结果和滚动时看到的不一样

原因:clearInterval和读取textContent之间有延迟,或者停止逻辑里又调了一次随机。

解决:停止时先clearInterval,然后直接读当前nameBox.textContent作为结果,不要再调pickOne。顺序反了就会出现「看到的和抽到的不一致」,现场会有人质疑。

4.5 现象:投影上字太小,后排看不见

原因:用了固定px字号,或者vw在低分辨率设备上算出来偏小。

解决:用clamp设下限,并且现场提前试投。如果实在看不清,把名字区域改成整屏显示,按钮缩小放角落,保证名字最大化。

5. 进阶技巧:把抽奖结果导出成表格,以及一个我常用的现场习惯

抽奖结束后,行政通常要一份中奖名单发通知,所以导出功能比想象中重要。最简单的做法是用Blob生成一个 csv 文件,点一下就能下载。

function exportWinners(winners) { // winners 是数组,每项 { name, prize } const header = '姓名,奖项\n'; const rows = winners.map(w => `${w.name},${w.prize}`).join('\n'); const blob = new Blob([header + rows], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '中奖名单.csv'; a.click(); URL.revokeObjectURL(url); }

type里的charset=utf-8必须加,否则 Excel 打开中文会乱码。URL.revokeObjectURL是释放内存,虽然不写也不影响功能,但养成习惯没坏处。导出的 csv 可以直接用 Excel 打开,也可以再转成 xlsx。

另一个我自己的习惯是:现场一定准备两个版本,一个正常版,一个「老板临时改规则」版。改规则版里把奖项数量、每轮抽几个人都做成输入框,而不是写死在代码里。年会现场变数太多,能少改一次代码就少一次翻车机会。

<label>本轮抽取人数:<input id="countInput" type="number" value="1" min="1"></label>
const count = parseInt(document.getElementById('countInput').value, 10) || 1; const winners = []; for (let i = 0; i < count; i++) { const w = pickOne(); if (w) winners.push(w); }

这个输入框看着不起眼,但真到现场,老板说「三等奖一次抽 10 个」的时候,你只需要改个数字,不用碰代码。我吃过这个亏,所以现在做任何抽奖页都会留这个口子。

最后说个验证方法:抽奖前用一份 100 人的测试名单跑 20 轮,看有没有重复、有没有漏抽、导出是否正常。跑完把drawn清空再上正式名单。这个流程花不了五分钟,但能挡掉九成的现场事故。希望帮到你。

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

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

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

立即咨询