纯前端年会抽奖系统:单HTML文件、零依赖、行政可维护
简介:这是一款专为中小企业年会场景设计的轻量级HTML抽奖工具,面向具备基础前端知识的技术人员或活动策划者,解决现场互动抽奖环节缺乏定制化、易部署方案的问题。资源包共22个文件,含2个HTML主页面、7个JavaScript逻辑脚本(含二维码生成与抽奖核心算法)、1个CSS样式表、8个PNG与4个JPG视觉素材,整体仅178KB,解压即用,无需服务器环境。已有1355人学习下载,体现了其在小型线下活动中对快速落地与低成本实施的实用价值。用户可直接编辑HTML与JS源码调整奖项名称、数量、中奖概率及参与者名单,配套图片资源已按功能分类存放,目录结构简洁清晰,便于理解交互流程与二次开发;尤其适合希望自主掌控抽奖规则、避免依赖第三方平台的技术型组织者。
1. 公司年会抽奖软件(静态html):不装环境、不连服务器、双击即开,5分钟配好3轮奖池,行政同事自己就能改中奖名单
去年年底帮某公司做年会支持,现场大屏突然黑屏——不是代码崩了,是IT临时被叫去处理打印机卡纸,没人会改那个用Python写的抽奖程序。最后靠Excel手动滚动名单凑合完事,台下笑声里带着一丝尴尬。这项目就是为这种场景生的:纯前端实现,零依赖,所有逻辑压进一个 HTML 文件,连 Node.js 都不用装。它不追求炫酷3D转盘或实时WebSocket同步,而是把「稳定展示」「快速配置」「防误操作」做到底——中奖名单直接写在 HTML 的<script>里,奖品名称、数量、中奖人姓名全可手改;点击“开始”后倒计时自动触发随机选取,结果高亮锁定,再点“重抽”才允许覆盖;连字体大小、按钮颜色、背景图路径都预留了注释明确的变量位置。适合行政、HR、活动策划这类非技术角色日常维护,也适合开发当模板二次封装成内网系统组件。如果你正被“年会前2小时发现抽奖程序打不开”折磨过,这个静态HTML包就是你的后悔药。
2. 技术选型与架构设计:为什么只用HTML/CSS/JS?三个硬约束倒逼出极简方案
2.1 核心约束决定技术栈:离线可用、零部署、非技术人员可维护
这个项目的技术决策不是“选最潮的”,而是被三个现实条件死死卡住:第一,年会场地网络不可靠,可能断网或DNS异常;第二,现场没有运维人员,不能临时起服务、配Nginx、开端口;第三,最终维护者是行政同事,她不会改Python脚本,但能看懂<h2>一等奖:iPhone 15</h2>这种标签。所以放弃所有需要运行时环境的方案:Node.js服务端渲染?断网就废;Vue单页应用打包?得配webpack和dev-server;甚至jQuery都砍掉——它虽小,但多一个CDN请求就多一分失败风险。最终选择原生三件套:HTML定义结构(含内联数据)、CSS控制视觉(响应式适配大屏/笔记本)、JS实现逻辑(ES6语法但兼容IE11)。所有资源内联:CSS写在<style>里,JS写在<script>里,连背景图都用base64编码塞进CSS,彻底消灭外部HTTP请求。整个文件就是一个.html,双击浏览器打开即用,关机重启后照样跑。
2.2 目录结构与文件构成:单文件即全部,但内部有清晰分层
项目实际交付物只有一个文件:lottery.html(约187KB)。别被“单文件”骗了——它内部按功能划分为四个逻辑区块,用注释严格隔离:
<!-- =============== 1. 页面结构与基础样式 =============== --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>公司年会抽奖系统</title> <style>/* 内联CSS:包含flex布局、大屏适配媒体查询、按钮悬停动画 */</style> </head> <body> <div id="app">...</div> <!-- 主容器,含标题、奖池切换Tab、当前奖池显示区、控制按钮 --> </body> <!-- =============== 2. 奖池数据配置区(行政可直接修改) =============== --> <script> // 【重要】此处为唯一需人工编辑区域! const PRIZES = [ { id: 'first', name: '一等奖', count: 3, items: ['iPhone 15', 'AirPods Pro', '戴尔XPS笔记本'] }, { id: 'second', name: '二等奖', count: 10, items: ['小米手环8', '罗技MX Master 3S', 'Bose QuietComfort 耳机'] } ]; // 中奖名单数组:格式为 [{name:'张三', prize:'一等奖'}, ...] let WINNERS = []; </script> <!-- =============== 3. 核心抽奖逻辑(含防重复、状态锁、动画) =============== --> <script> function startLottery(prizeId) { const prizePool = PRIZES.find(p => p.id === prizeId); if (!prizePool) return; // 状态锁:防止连点导致重复抽中同一人 if (isDrawing) return; isDrawing = true; // 从剩余未中奖名单中随机取一个(关键:排除已中奖者) const candidates = ALL_STAFF.filter(name => !WINNERS.some(w => w.name === name) ); if (candidates.length === 0) { alert('所有人已中奖完毕!'); isDrawing = false; return; } // 动画模拟:每100ms刷新一次随机名字,持续1.5秒 const interval = setInterval(() => { const randomName = candidates[Math.floor(Math.random() * candidates.length)]; document.getElementById('current-name').textContent = randomName; }, 100); setTimeout(() => { clearInterval(interval); const winnerName = candidates[Math.floor(Math.random() * candidates.length)]; WINNERS.push({ name: winnerName, prize: prizePool.name }); renderWinners(); // 刷新中奖名单列表 isDrawing = false; }, 1500); } </script> <!-- =============== 4. 初始化与事件绑定(页面加载即生效) =============== --> <script> // 页面加载后自动执行 document.addEventListener('DOMContentLoaded', () => { initPrizeTabs(); // 渲染奖池Tab栏 renderWinners(); // 渲染历史中奖名单 bindEventListeners(); // 绑定按钮点击事件 }); </script>提示:所有业务逻辑都集中在
<script>区块内,但通过注释明确划分“配置区”“逻辑区”“初始化区”。行政同事只需修改PRIZES数组里的name和items,以及ALL_STAFF数组里的姓名列表(见下文),其他部分完全不动。
2.3 数据模型设计:为什么用数组而非JSON文件?离线容错优先级最高
很多人第一反应是“把中奖名单存成JSON文件”,但这里刻意避免:JSON文件需要AJAX读取,而AJAX在file://协议下会被现代浏览器拦截(CORS策略)。所以所有数据都硬编码在JS变量里:
ALL_STAFF: 字符串数组,存储全员姓名,如['张三', '李四', '王五']。这是抽奖的“源池”,必须保证无重复、无空格、无特殊字符。PRIZES: 对象数组,每个对象含id(唯一标识)、name(显示名称)、count(计划抽取人数)、items(奖品清单,用于中奖后展示)。WINNERS: 运行时数组,存储已中奖者对象{name, prize},页面刷新后丢失——这反而是优点:避免上届年会数据残留,强制每次重新开始。
为什么不用localStorage持久化?因为行政同事可能用不同电脑打开,且“清空历史”是高频操作。我们把“易重置”当作核心体验,而不是“自动保存”。
3. 快速运行指南:从下载到大屏展示,三步完成(含大屏适配实操)
3.1 下载与本地验证:确认文件完整性与基础功能
下载解压后,你将得到一个lottery.html文件。不要上传到任何服务器,直接双击用Chrome/Firefox/Edge打开即可。首次打开时检查三件事:
- 页面顶部是否显示“公司年会抽奖系统”标题;
- 左侧是否出现“一等奖”“二等奖”等Tab按钮;
- 点击任意Tab后,下方是否显示对应奖品名称(如“iPhone 15”)。
若以上任一失败,请右键→“查看网页源代码”,搜索关键词<script>,确认是否存在const PRIZES = [这一行。如果整段JS被删改或格式错误(如少了个]),浏览器控制台(F12→Console)会报错Uncaught SyntaxError,此时需用记事本重新粘贴原始JS区块。
注意:Windows默认用IE打开HTML文件,而IE对ES6语法(如
const、箭头函数)支持极差。务必右键→“打开方式”→选择Chrome或Edge。Mac用户同理,避免用Safari打开。
3.2 配置全员名单与奖池:行政同事可操作的两处关键修改
打开lottery.html,用VS Code或记事本(不要用Word)编辑。找到注释<!-- =============== 2. 奖池数据配置区(行政可直接修改) =============== -->下方的JS代码块。
第一步:修改全员名单(ALL_STAFF)
搜索ALL_STAFF = [,将其替换为你的公司真实姓名列表。注意格式:
- 每个姓名用英文单引号包裹,如
'张三'; - 姓名间用英文逗号分隔,末尾不加逗号;
- 禁止使用中文顿号、空格、括号,例如
'张三(技术部)'会导致抽奖失败; - 示例正确写法:
const ALL_STAFF = [ '张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十', '郑十一', '王十二' ];
第二步:增删奖池(PRIZES)
继续在此区块内,修改PRIZES数组。每个奖池对象必须包含id(小写字母+数字,如'luckydraw')、name(显示名)、count(计划抽取人数)、items(奖品数组)。新增奖池直接在数组末尾加逗号分隔的对象:
const PRIZES = [ { id: 'first', name: '一等奖', count: 3, items: ['iPhone 15', '戴尔XPS笔记本'] }, { id: 'second', name: '二等奖', count: 10, items: ['小米手环8', 'Bose耳机'] }, // 新增三等奖(注意前面的逗号!) { id: 'third', name: '三等奖', count: 30, items: ['Kindle Paperwhite', '罗技鼠标', '小米充电宝'] } ];提示:
count值仅作提示用,不影响实际抽取逻辑(系统会一直抽到手动停止)。真正限制中奖人数的是WINNERS.length与ALL_STAFF.length的关系。
3.3 大屏投屏实操:解决字体模糊、按钮太小、滚动条遮挡三大现场问题
年会现场用投影仪或LED大屏时,常遇到文字发虚、按钮点不准、名单列表滚动条盖住姓名等问题。解决方案全在CSS内联样式中,无需额外文件:
字体模糊?在
<style>区块搜索font-family,将默认的"Microsoft YaHei"改为"PingFang SC","Hiragino Sans GB"(Mac)或保留原值,关键是添加-webkit-font-smoothing: antialiased;强制抗锯齿:body { font-family: "Microsoft YaHei", sans-serif; -webkit-font-smoothing: antialiased; /* 解决Chrome Mac字体发虚 */ }按钮太小?搜索
.btn类,增大padding和font-size:.btn { padding: 24px 48px; /* 宽高都加大 */ font-size: 28px; /* 字体放大 */ min-width: 200px; /* 防止文字过长时按钮压缩 */ }滚动条遮挡姓名?搜索
#winners-list,添加自定义滚动条样式(隐藏默认滚动条,用纯色块替代):#winners-list { overflow-y: auto; max-height: 400px; } #winners-list::-webkit-scrollbar { width: 12px; } /* Chrome/Safari */ #winners-list::-webkit-scrollbar-track { background: #f1f1f1; } #winners-list::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 6px; }
实测经验:用Chrome浏览器按Ctrl + Shift + M进入设备模拟模式,选择1920x1080分辨率,再按Ctrl + Plus放大到150%,即可预演大屏效果。现场投屏时,建议提前用此模式调好缩放比例,避免上台后手忙脚乱。
4. 常见问题排查:行政同事现场翻车的5个真实场景及解法
4.1 现象:点击“开始抽奖”没反应,控制台报错Uncaught ReferenceError: startLottery is not defined
原因:startLottery函数定义在<script>区块内,但该区块被意外删除或注释掉了。常见于用Word编辑HTML文件,Word会自动将半角引号转为全角,导致JS语法错误。
解决:用记事本或VS Code打开文件,搜索function startLottery,确认该函数存在且未被<!--注释包裹。若发现类似<!-- function startLottery... -->,删除首尾<!--和-->。
4.2 现象:抽奖时名字疯狂滚动但停不下来,或永远停在同一个名字
原因:setTimeout时间(1500ms)与setInterval间隔(100ms)不匹配,或candidates数组为空。后者更常见——ALL_STAFF名单里有重复姓名,或WINNERS数组已满,导致candidates.length === 0。
解决:按F12打开开发者工具→Console,输入ALL_STAFF.length和WINNERS.length,对比是否相等。若相等,说明全员已中奖,需清空WINNERS = []并刷新页面;若ALL_STAFF有重复,用Excel的“删除重复项”功能清洗后再粘贴回HTML。
4.3 现象:大屏显示时,中奖名单右侧出现白色滚动条,遮挡最后几个字
原因:#winners-list容器的max-height设置过小,或内部<li>元素margin过大,撑高了内容高度。
解决:在<style>区块中找到#winners-list,将max-height从300px改为400px;同时搜索.winner-item,将margin-bottom: 12px改为margin-bottom: 8px,释放垂直空间。
4.4 现象:切换奖池Tab时,下方奖品名称不更新,始终显示第一个奖池内容
原因:initPrizeTabs()函数中,事件监听器绑定错误。常见错误是给所有Tab按钮绑定了同一个prizeId,导致点击任意Tab都触发首个奖池。
解决:搜索initPrizeTabs函数,确认循环内是否正确获取当前按钮的>tabs.forEach(tab => { tab.addEventListener('click', () => { const prizeId = tab.getAttribute('data-id'); // 关键:必须从当前tab取data-id showPrizeDetails(prizeId); }); });
若代码中写成const prizeId = tabs[0].getAttribute('data-id'),则需修正。
4.5 现象:中奖后姓名显示为undefined,或控制台报错Cannot read property 'name' of undefined
原因:candidates数组为空,但代码未做兜底处理,直接执行candidates[Math.floor(...)]返回undefined。
解决:在startLottery函数内setTimeout前添加防御性判断:
if (candidates.length === 0) { document.getElementById('current-name').textContent = '所有人已中奖完毕!'; isDrawing = false; return; }此补丁已在标准版中内置,若你使用的是旧版本,请手动添加。
5. 二次开发实战:从“能用”到“好用”的3个改造技巧(含代码)
5.1 改造一:增加“中奖人部门”字段,让大屏显示更专业
年会现场领导常问“哪个部门中奖最多?”,原版只显示姓名,无法统计。我们扩展ALL_STAFF数据结构,从字符串数组升级为对象数组,并修改渲染逻辑。
步骤1:修改数据源
将ALL_STAFF从:
const ALL_STAFF = ['张三', '李四'];改为:
const ALL_STAFF = [ { name: '张三', dept: '技术部' }, { name: '李四', dept: '市场部' } ];步骤2:修改抽奖逻辑
在startLottery函数中,candidates筛选和winnerName赋值需同步更新:
// 原代码(只取name) const winnerName = candidates[Math.floor(Math.random() * candidates.length)]; // 新代码(取完整对象) const winner = candidates[Math.floor(Math.random() * candidates.length)]; WINNERS.push({ name: winner.name, dept: winner.dept, prize: prizePool.name });步骤3:修改HTML渲染
找到中奖名单渲染函数renderWinners(),将原本的<li>${w.name}</li>改为:
<li>${w.name} <span class="dept">(${w.dept})</span></li>并在CSS中添加.dept { color: #ff6b6b; font-weight: bold; }突出显示。
效果:大屏上姓名后带红色部门标注,如“张三(技术部)”,方便主持人播报时强调归属。
5.2 改造二:添加“撤回上一人”功能,救场行政同事的手滑
现场常有“刚点完就发现念错了名字”情况。原版只能重抽,但新中奖者可能已上台。我们增加一个“撤回”按钮,将最后一名中奖者移出WINNERS数组并恢复其可抽状态。
步骤1:添加按钮HTML
在控制按钮区(搜索<div class="controls">),插入新按钮:
<button type="button" class="btn btn-danger" id="undo-btn">撤回上一人</button>步骤2:编写撤回逻辑
在JS逻辑区添加函数:
document.getElementById('undo-btn').addEventListener('click', () => { if (WINNERS.length === 0) return; const lastWinner = WINNERS.pop(); // 移除最后一个 // 无需其他操作:因ALL_STAFF是源数据,移除WINNERS即自动恢复可抽状态 renderWinners(); document.getElementById('current-name').textContent = '已撤回:' + lastWinner.name; setTimeout(() => { document.getElementById('current-name').textContent = ''; }, 2000); });步骤3:增强视觉反馈
在CSS中添加.btn-danger样式:
.btn-danger { background-color: #e74c3c; border-color: #c0392b; } .btn-danger:hover { background-color: #c0392b; }血泪经验:此功能上线后,某公司行政同事在三等奖环节手滑点了两次,靠“撤回”救回一个名额,全场鼓掌——技术的价值有时就藏在一个按钮里。
5.3 改造三:导出中奖名单为CSV,方便财务后续发奖
年会结束后,财务需要Excel名单登记奖品发放。手动复制粘贴易出错,我们增加一键导出功能。
步骤1:添加导出按钮
在控制区添加:
<button type="button" class="btn btn-export" id="export-btn">导出名单为CSV</button>步骤2:实现CSV生成逻辑
在JS中添加:
document.getElementById('export-btn').addEventListener('click', () => { if (WINNERS.length === 0) return; // 构建CSV内容:表头+数据行 let csvContent = "data:text/csv;charset=utf-8," + "姓名,部门,奖项\n"; WINNERS.forEach(w => { // 处理姓名中的逗号(CSV转义规则) const escapedName = w.name.includes(',') ? `"${w.name}"` : w.name; csvContent += `${escapedName},${w.dept || ''},${w.prize}\n`; }); const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", "年会中奖名单.csv"); document.body.appendChild(link); link.click(); document.body.removeChild(link); });步骤3:微调CSS
添加.btn-export样式,用绿色区别于其他按钮:
.btn-export { background-color: #2ecc71; border-color: #27ae60; }注意:此导出功能依赖浏览器的
<a download>属性,在Safari中可能失效(需用户手动另存为),但Chrome/Edge/FF均完美支持。若需全浏览器兼容,可引入轻量库Papaparse,但会增加文件体积,权衡后暂不引入。
从那以后我每次给客户交付抽奖系统,都会在README.md里手写一行:“撤回按钮和CSV导出是行政同事的救命稻草,请务必测试”。因为真正的工程落地,不在于算法多精妙,而在于能否接住人类手滑的瞬间。希望帮到你。
本文还有配套的精品资源,点击获取