纯前端年会抽奖系统:单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 langzh-CN head meta charsetUTF-8 title公司年会抽奖系统/title style/* 内联CSS包含flex布局、大屏适配媒体查询、按钮悬停动画 *//style /head body div idapp.../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 GBMac或保留原值关键是添加-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 classdept(${w.dept})/span/li并在CSS中添加.dept { color: #ff6b6b; font-weight: bold; }突出显示。效果大屏上姓名后带红色部门标注如“张三技术部”方便主持人播报时强调归属。5.2 改造二添加“撤回上一人”功能救场行政同事的手滑现场常有“刚点完就发现念错了名字”情况。原版只能重抽但新中奖者可能已上台。我们增加一个“撤回”按钮将最后一名中奖者移出WINNERS数组并恢复其可抽状态。步骤1添加按钮HTML在控制按钮区搜索div classcontrols插入新按钮button typebutton classbtn btn-danger idundo-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 typebutton classbtn btn-export idexport-btn导出名单为CSV/button步骤2实现CSV生成逻辑在JS中添加document.getElementById(export-btn).addEventListener(click, () { if (WINNERS.length 0) return; // 构建CSV内容表头数据行 let csvContent data:text/csv;charsetutf-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导出是行政同事的救命稻草请务必测试”。因为真正的工程落地不在于算法多精妙而在于能否接住人类手滑的瞬间。希望帮到你。本文还有配套的精品资源点击获取