简介:一款基于静态HTML开发的公司年会抽奖工具,面向具备HTML/CSS/JavaScript基础、希望自行定制抽奖环节的策划或开发人员。压缩包内共22个文件,包含2个HTML页面、1个CSS样式表、7个JS脚本以及8个PNG和4张JPG图片,整体仅178KB,结构与典型前端小项目一致,轻量易部署。其中JS脚本集成了抽奖核心逻辑与二维码模块(qrcode.min.js),方便在现场进行扫码互动或名单展示。通过修改源码中的奖项数据、参与名单和随机算法,即可适配不同规模的活动;同时界面图片资源可直接替换,快速换肤。目前已有1353人学习,对于需要快速获得年会抽奖方案且能接受代码自定义的用户来说,这套静态页面源码比在线工具更灵活,也便于离线运行和数据隐私保护。 每到年底,总有同事跑过来问:“年会抽奖软件你搞不搞?”往年我都是临时从网上找一个在线抽奖页面凑合,结果现场经常碰到网络卡顿、弹广告,甚至抽奖页面突然打不开的尴尬。今年我下定决心,用纯静态HTML自己做了一套公司年会抽奖软件——整个程序就是一个HTML文件,不依赖后端服务,也不依赖网络,双击打开浏览器就能跑。这篇文章把完整的方案设计、核心代码和现场踩坑记录都分享出来,特别适合年底需要给公司年会做抽奖程序、又不想引入复杂系统的同学参考。
1. 为什么选静态HTML:年会抽奖的底层需求
1.1 年会现场的第一原则是稳定,不是炫酷
年会抽奖这个场景,本质上不是软件工程问题,而是“大型活动现场的可靠性问题”。想象一下,几百双眼睛盯着大屏幕,主持人喊“开始抽奖”,结果页面白屏十秒钟没有反应,后台负责操作的同事汗直接就下来了。我做过几年活动支持,最深的体会是:抽奖软件最大的卖点不是动画有多炫,而是不出事。
静态HTML方案正好命中这个痛点。所谓静态HTML,通俗说就是整个应用只有一个HTML文件,里面把结构、样式、逻辑全部写完,浏览器打开就能运行。它不依赖服务器,不依赖数据库,不需要安装运行环境,甚至不需要外网。这样就把年会现场最大的不确定性——网络和外部系统,直接干掉了。
你可能会问,现在抽奖平台那么多,为什么要自己做?很多平台功能确实全,抽奖、签到、大屏展示都有,但有两个常见问题:第一,很多公司年会是在酒店宴会厅办的,现场Wi-Fi质量捉摸不定,一旦掉线,整个环节就凉了;第二,把全公司员工名单传到第三方网站上,很多人心里是打鼓的,HR也会担心隐私问题。静态HTML方案把这两件事都解决了:名单就在本地文件里,数据不出电脑;打开文件就是应用,完全不依赖外网。
1.2 对比在线平台和桌面程序,静态方案赢在哪
之前有个同事用Python写过一个抽奖工具,功能没问题,但年会现场那台电脑缺Python环境,光装环境就花了十分钟。后来我们尝试过Electron打包的桌面软件,体积倒是其次,关键是那台老机器跑起来一卡一卡,界面响应很慢。静态HTML就没有这些毛病,双击打开就是应用,代码就是文件本身,天然不会出现环境问题。
为了说清楚,我整理了一份三种方案的对比:
| 对比项 | 在线抽奖平台 | 桌面程序 | 静态HTML |
|---|---|---|---|
| 网络依赖 | 强依赖现场网络 | 不依赖 | 不依赖 |
| 部署难度 | 注册、配置、适配 | 安装运行环境 | 打开即用 |
| 数据隐私 | 名单上传第三方 | 数据本地 | 数据本地 |
| 定制能力 | 受平台功能限制 | 较强 | 可改所有代码 |
| 现场容错 | 依赖平台稳定性 | 依赖本机环境 | 容错最高 |
| 文件体积 | 无 | 通常几十MB以上 | 几十KB |
静态HTML还有一个容易被忽视的优点:可定制性极强。公司主题色、Logo、背景音乐、抽奖动画节奏,全部自己改代码就能实现,不需要等第三方平台更新功能。甚至可以塞入“开场倒计时”“中奖播报语音”这些定制需求。所以如果你的需求是“年会前两周快速搞定,现场稳定优先”,静态HTML几乎是性价比最高的选择。
2. 核心设计:一个抽奖软件到底在做什么
2.1 名单与奖项的数据结构设计
年会抽奖最常见的需求有三类:名单导入、分奖项抽取、中奖名单展示。名单的来源通常是HR给的Excel表格,第一版可以直接写在JS数组里,后续再改成读取外部文件。数据结构建议这样设计:
// 员工名单:从Excel手动转成这个格式 const allStaff = [ { name: "张三", department: "研发部", id: "001" }, { name: "李四", department: "市场部", id: "002" }, // 后续按这个格式继续追加 ]; // 奖项配置:包含奖项名、数量、抽取顺序 const awards = [ { name: "一等奖", count: 3, order: 3 }, { name: "二等奖", count: 8, order: 2 }, { name: "三等奖", count: 20, order: 1 }, ];这里有两个细节要注意。第一,奖项顺序必须能配置,因为现场通常先抽三等奖,再抽二等奖,最后抽一等奖,刺激感是层层递进的。第二,名单字段里最好带上部门和工号,中奖展示时显示“张三 研发部”,现场更有仪式感,也方便确认身份,避免同名同姓引发的尴尬。
我在实际开发中还加了一个tag字段,用来标记员工的特殊属性。比如“最佳新人奖只能从入职一年内的员工里抽”,抽奖前按tag过滤奖池即可,代码改动很小,但现场灵活性高了很多。年会策划那边临时提出“这个奖项要从特定部门抽”的需求,我改一行配置就能上线。
2.2 抽奖逻辑:不重复抽中的实现方式
很多人第一次写抽奖逻辑,会犯一个经典错误:每次从总名单里随机取一个人,取完之后不把这人移除,导致下一轮抽到同一个名字。年会抽奖必须区分“放回抽样”和“不放回抽样”。正确做法是:抽奖前先把总名单复制一份作为“奖池”,每抽中一个人,就把他从奖池里移除。
核心逻辑可以概括为这么几步:
- 初始化奖池:从总名单中过滤掉所有已经中奖的人;
- 随机抽取:在奖池中用随机数选一个下标;
- 移除中奖者:把抽中的人从奖池中splice移除;
- 记录结果:把中奖者追加到中奖名单数组。
// 随机抽取一个(不放回) function pickOne() { if (pool.length === 0) return null; const idx = Math.floor(Math.random() * pool.length); const winner = pool.splice(idx, 1)[0]; winners.push(winner); return winner; }这里有一个容易被忽视的进阶需求:当三等奖抽完20个人之后,这20个人在二等奖抽奖时必须被排除。如果每一轮抽奖前都重新调用初始化奖池的逻辑,从总名单里过滤已中奖名单,这个需求就能自然满足。我建议把所有奖项抽奖前都统一走一次initPool,传入当前奖项的过滤条件,代码逻辑统一,后边才不会出幺蛾子。
2.3 大屏展示与现场操作节奏
现场操作分两部分:一部分是操作员在小屏幕上使用的控制区,一部分是投影到大屏幕的展示区。因为整个程序是静态HTML,推荐用同一个页面同时承载两个区域:页面上方是大屏展示区,页面右侧是控制按钮。
按钮核心就三个:下一轮抽奖、开始滚动、停止揭晓。为什么不是直接“抽一下出结果”?因为年会抽奖需要让屏幕上的名字快速滚动,制造紧张感,再由主持人喊停或操作员按键停止。这个“先滚起来再停下”的过程,是年会抽奖的气氛灵魂。
名册滚动动画我用setInterval实现,200毫秒刷新一次显示内容,随着场内气氛升温,滚动速度可以调到60到80毫秒,效果更刺激。停止时再随机选取一个人,用CSS做一个放大闪光的动画,配合现场音乐达到高潮效果。整个过程代码量不大,但节奏控制好了,现场氛围会完全不一样。
3. 代码落地:从零写出可上线的静态抽奖页面
3.1 单文件页面结构与全屏样式
我建议把整个项目做成“单文件”,也就是一个HTML文件内包含CSS和JS,这样拷到哪都能用。页面布局采用全屏大背景,中间是待抽奖区,右侧是控制面板。核心HTML结构如下:
<div id="screen"> <div id="currentAward">二等奖</div> <div id="nameArea"> <span id="rollingName">准备开始</span> </div> </div> <div id="controlPanel"> <select id="awardSelect"></select> <button id="btnRoll">开始滚动</button> <button id="btnStop">停止揭晓</button> <button id="btnReset">重置本轮</button> <button id="btnFullscreen">全屏</button> </div>样式方面有几个实用建议。大屏文字要足够大,姓名至少用96px以上,保证坐在最后一排的人也能看清;背景用深色或公司主题色,白色文字对比度高。中奖后可以加一个金色边框的CSS动画,现场视觉冲击力更强。我偏好用渐变背景加粒子效果,但粒子效果不能太密集,否则反而干扰名字显示。
控制面板和展示区必须视觉分离。我通常用媒体查询,在普通电脑屏幕上让控制面板靠右固定,在大屏显示器上自动隐藏操作按钮。操作员手里可以再准备一个无线翻页笔,配合键盘快捷键控制,比鼠标更不易出错。
3.2 随机抽取、滚动动画与结果揭晓
接下来是核心JS逻辑。我会把“随机抽取”和“动画展示”分开写,避免动画卡住时影响真实数据。完整逻辑如下:
// 全局状态 let pool = []; let rollTimer = null; let currentWinner = null; // 初始化奖池:从总名单里剔除已中奖者 function initPool() { const winnersSet = new Set(winners.map(w => w.id)); pool = allStaff.filter(p => !winnersSet.has(p.id)); } // 开始滚动:只是名字快速切换,不落定 function startRoll() { if (pool.length === 0) { alert("奖池已空,请确认是否还有未抽奖项"); return; } rollTimer = setInterval(() => { const temp = pool[Math.floor(Math.random() * pool.length)]; document.getElementById("rollingName").textContent = temp.name; }, 80); } // 停止滚动:真正决定中奖者 function stopRoll() { if (!rollTimer) return; clearInterval(rollTimer); rollTimer = null; currentWinner = pickOne(); document.getElementById("rollingName").textContent = currentWinner.name; appendWinnerToWall(currentWinner, currentAwardName); }这段代码有两个容易被忽略的细节。第一,开始滚动时显示的名字是用随机数临时取的,不等于最终中奖者;停止滚动时才真正从奖池里抽人。这样做的好处是,用户可以享受“名字滚动”的过程,而结果完全随机,不会出现“看着名字滚动但最后中奖人不是屏幕上的名字”这种逻辑bug。
第二,stopRoll里才调用pickOne,此时才从pool里splice,这样保证后续抽奖不会重复。如果在startRoll时就把滚动中显示过的名字全部排除,会大大缩小奖池,抽奖就会失真。这个细节我当时调了很久才意识到。
3.3 中奖墙与记录持久化
中奖名单要实时追加到大屏下方的“中奖墙”上。我通常做成两列网格,持续展示当前奖项已经抽出的名单。如果是三等奖要抽20人,屏幕上最好能同时看到20个中奖者的名字,而不是只显示最新一个。
function appendWinnerToWall(winner, awardName) { const wall = document.getElementById("winnerWall"); const card = document.createElement("div"); card.className = "winner-card"; card.innerHTML = `<span class="w-name">${winner.name}</span> <span class="w-dept">${winner.department}</span> <span class="w-award">${awardName}</span>`; wall.appendChild(card); }这里有个现场经验:中奖墙卡片不要做得太大,否则一等奖还没抽,前面三等奖的卡片已经占满大半屏。我的习惯是三等奖用小型列表,二等奖用卡片,一等奖用大号卡片,视觉上有层次感,也符合现场逐步推向最高潮的节奏。
考虑到操作员可能误刷新页面,我会把每一轮的中奖结果写入localStorage。刷新后页面自动恢复中奖名单,已经抽过的人不会再次进入奖池,这个功能在彩排和正式场之间切换时尤其有用。
3.4 进一步:静态HTML如何打包发布
有同学问过,静态HTML能不能像后端程序一样统一打包发布?答案是可以的。如果你公司有内网服务器,想在多个分会场同时访问同一个抽奖地址,最简单的方式是用Spring Boot托管静态文件:把HTML文件放到resources/static目录下,然后打包成一个可执行jar包,启动后访问localhost:8080就能看到抽奖页面。
这种方式本质上是用后端容器托管静态资源,对纯静态方案来说只是换了一种打开方式。如果你只是想在年会现场用一台电脑投屏,完全不需要打包;但如果你想做多会场联动、集中管理名单和抽奖结果,那就需要一个后端来同步数据,静态HTML更适合当作前端界面来用,后端可以逐步加接口。
4. 实战复盘:那些容易翻车的细节
4.1 中奖名单重复,问题多半出在奖池没过滤
这是抽奖软件最经典的翻车现场。根本原因往往不是随机算法有问题,而是奖池没有过滤已经中奖的人。我在2.2节已经给出了正确做法,但这里要说一个更隐蔽的情况:多个奖项之间存在顺序时,过滤范围要特别小心。
举例:一等奖、二等奖、三等奖都从全体员工里抽,那么抽完三等奖后,这20个人就不应该再出现在二等奖的奖池里。我在代码里统一封装了initPool,每次抽奖前调用,从总名单.filter掉所有winners中已存在的id,这样按顺序抽奖天然就不会重复。而如果某个奖项有独立候选人范围,就额外传入一个过滤函数,代码仍然统一。
4.2 中文名单乱码与隐藏字符
静态HTML如果文件编码出了问题,中文名单大概率变成乱码,这在几百人盯着的屏幕上就是“事故现场”。解决方法有两个:第一,保存文件时选择UTF-8编码;第二,在HTML的<head>里明确声明<meta charset="UTF-8">。两个都要做,缺一个都可能踩坑。
如果你是从Excel复制名单到JS数组,粘贴时经常带着看不见的BOM字符或特殊空格。我建议用正则过滤一遍不可见字符:
// 清理名单里的隐藏字符 const cleanStr = str.replace(/[\u200B-\u200D\uFEFF]/g, "").trim();这个坑我踩过一次:彩排时有个名字怎么都显示不出来,排查了半天才发现是Excel粘贴时带入了一个零宽空格。看似很小的字符问题,在正式年会上就会变成大麻烦。
4.3 现场突发操作与数据备份
年会现场最怕的不是软件bug,而是人为操作失误。比如名单更新了但U盘里还是旧版,比如主持人让多抽一个但对应奖项已经抽满,比如误刷新导致当前状态丢失。我的建议是每次年会前做三件事:
- 抽奖名单、HTML文件、备份源文件放在同一个U盘的独立目录里,同时邮箱给自己发一份;
- 在现场电脑上提前30分钟用真实名单完整跑一遍流程,包括抽空某个奖项、重置、撤销等操作;
- 准备一份“紧急加人”方案:现场临时要加人,直接在JS数组里复制粘贴几行,保存刷新即可,不需要重启服务。
另外,抽奖过程中要保留一个“撤销本轮”按钮。万一主持人报错号码或者抽错部门,操作员可以快速回退上一轮,把中奖者从winners里移除,重新放回奖池。这个按钮在彩排时可能用不上,但正式场一旦出现人为失误,它就是救命稻草。
4.4 音效播放限制与多屏协作
抽奖软件的音效通常用HTML的Audio来播放。但要注意浏览器的自动播放策略:用户必须至少点击过一次页面之后才能播放声音,否则会被浏览器拦截。解决方案是:在操作员第一次点击“开始滚动”时,主动创建一个AudioContext或者调用一次play()方法,建立用户交互通道,之后再播放音效就不会被拦截。
多屏协作方面,我建议在控制按钮上加一个“全屏”按钮,调用document.documentElement.requestFullscreen(),避免现场手忙脚乱去按F11。如果有多块屏幕,可以打开两个浏览器窗口:一个操作窗口,一个投影窗口。操作窗口上把控制面板和展示区分离,投影窗口只显示大屏区域,这样按钮就不会出现在大屏上,视觉效果更干净。
如果你按上述方式把整个程序打包成一个文件,整个项目的体积通常只有几十KB,打开秒开,完全不依赖任何外部资源。这也是它能在年会上走得稳的原因之一:越简单的东西越难出问题。
说实话,这个项目本身并不复杂,几百行代码就能搞定,但真正考验人的不是代码,而是对现场各种意外情况的预判。我做完这套静态HTML抽奖软件,最大的收获是明白了一个道理:在年会上,抽奖程序的“稳定”和“准确”远比“炫酷”重要。如果你也想动手做一套,建议至少提前两周做好第一版,然后拉着几个同事用真实名单多试几轮,把抽空奖池、误刷新、名单撤回这些情况全部演练一遍。等到年会真正开始时,你站在控制台边上看着名字稳稳当当地停下来,就会明白:这套看起来朴素的静态HTML页面,比什么花哨的在线平台都让人安心。
本文还有配套的精品资源,点击获取