纯前端年会抽奖系统:单HTML文件、零依赖、行政可维护

发布时间:2026/10/26 17:49:36
纯前端年会抽奖系统:单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打开即可。首次打开时检查三件事:

  1. 页面顶部是否显示“公司年会抽奖系统”标题;
  2. 左侧是否出现“一等奖”“二等奖”等Tab按钮;
  3. 点击任意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导出是行政同事的救命稻草,请务必测试”。因为真正的工程落地,不在于算法多精妙,而在于能否接住人类手滑的瞬间。希望帮到你。

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