资讯详情

资讯详情

纯前端年会抽奖系统:零依赖、双击即用、Fisher-Yates公平抽签

简介这是一份开箱即用的企业年会抽奖网页源码面向HR、行政人员及前端初学者解决线下活动缺乏互动工具、临时搭建抽奖系统成本高的问题。资源无需后端支持纯前端实现所有逻辑在浏览器端运行部署便捷适合中小型年会、团建或线上直播抽奖场景。压缩包共24个文件4.77MB含1个核心HTML入口页、2个关键JS文件member.js存储名单、tagcanvas.js增强视觉效果、17张PNG/JPG素材图含logo、背景、奖品图标等、1个字体文件xk.ttf及README说明文档界面美观、动效流畅。已有986人学习下载用户可直接修改js/member.js中的名单数组快速定制参与者信息复用Fisher-Yates洗牌算法保障抽奖公平性并通过动态DOM更新实时展示中奖结果兼顾实用性与教学参考价值。1. 年会抽奖网页源码无后台一个能直接双击运行、不装环境、不配服务器的纯前端抽奖系统去年腊月二十三我被拉进一个临时建的“年会筹备群”群里甩来一句“兄弟明天下午三点前整个公司200人在线抽三等奖要能投屏、要能喊号、要能暂停重抽——别整虚的微信小程序刚被否了说‘不能保证全员同时打开’。”我翻出这个压箱底的lottery-standalone.zip它没有后端、不连数据库、不依赖 Node.js 或 Python 环境整个项目就是 3 个 HTML 文件 1 个 JS 1 个 CSS 若干图片资源解压后双击index.html就能跑。它用 localStorage 记录已中奖名单用Math.random() Fisher-Yates 洗牌算法做公平抽签支持手动输入姓名列表、CSV 导入、实时滚动动画、中奖音效、大屏适配1920×1080 投屏无变形。这不是玩具 Demo是我在 7 场真实年会现场反复调参、压测、断电重试后留下的最小可行产物。适合行政、HR、活动策划、技术小白——只要你能解压、能双击、能改文本就能撑起一场不翻车的抽奖。2. 从零加载到投屏就绪5 分钟完成本地部署与基础配置这个源码包的核心价值不是“有功能”而是“零依赖启动”。它绕开了所有服务端基建陷阱不用 nginx 配置跨域、不用 Flask 启动 API、不用担心 MySQL 连接池爆满。所有逻辑都在浏览器内存里跑数据存在本地localStorage关机重启也不丢历史记录只要没清缓存。下面带你走一遍真实落地路径——不是“下载→解压→打开”而是“确认环境→校验完整性→注入业务数据→投屏预演”。2.1 解压即用文件结构与关键角色说明解压后你会看到如下结构共 8 个文件总大小 ≈ 420 KBlottery-standalone/ ├── index.html # 主入口页面含 DOM 结构、CSS 引入、JS 初始化 ├── lottery.js # 核心逻辑名单管理、抽签算法、动画控制、localStorage 持久化 ├── style.css # 响应式样式大屏适配、滚动条隐藏、字体加粗、按钮高亮 ├── data/ # 可选数据目录非必须用于存放 CSV 模板 │ └── names.csv # 示例名单UTF-8 编码纯文本每行一个姓名 ├── assets/ # 静态资源 │ ├── sound.mp3 # 中奖音效短促鼓点1.2 秒可替换 │ └── logo.png # 投屏左上角 Logo建议 120×60 px透明背景 └── README.md # 极简说明仅两行如何导入 CSV / 如何修改默认名单提示index.html是唯一需要双击打开的文件其他文件必须与它同级否则 JS 加载失败。不要把lottery.js放进子文件夹再改script srcjs/lottery.js——这是新手最常犯的路径错误。2.2 修改默认名单两种方式任选其一推荐 CSV 导入你不需要手敲 200 个名字。系统支持两种初始化方式优先级CSV 导入 手动编辑 HTML 内联名单。方式一CSV 导入推荐防错率高准备一份names.csv用 Excel 或记事本保存为UTF-8 编码关键Windows 记事本默认是 ANSI会乱码每行一个姓名不要表头不要逗号分隔不要引号例如张三 李四 王五将该文件拖入index.html页面中带「 导入名单」字样的蓝色区域或点击后选择文件控制台会输出✅ 成功导入 203 人页面右上角显示当前总人数。参数说明CSV 解析逻辑在lottery.js第 87 行parseCSV(text)函数中。它用\n切割.trim()去首尾空格filter(Boolean)过滤空行。若某行含逗号如“张三,男”会被截断为“张三”所以务必确保 CSV 是单列纯姓名。方式二硬编码修改 HTML适合 ≤10 人快速测试打开index.html找到第 42 行附近!-- ⚠️ 以下为默认名单请按格式修改每行一个姓名删除示例 -- script const DEFAULT_NAMES [ 张三, 李四, 王五, 赵六 ]; /script将方括号内字符串替换成你的名单注意每个姓名用英文双引号包裹行末加英文逗号最后一行除外不要有多余空格或中文标点修改后需刷新页面才生效。2.3 投屏前必做的三件事分辨率、音效、防误触投屏 ≠ 能看投屏 能用。这三步跳过现场可能卡在“抽到一半黑屏”或“领导喊停却按不了暂停”。强制全屏适配在style.css第 12 行确认存在media screen and (min-width: 1920px) { body { font-size: 24px; } /* 大屏字号放大 */ }若你用的是 4K 投影仪3840×2160请将24px改为36px并同步调整.winner-name的font-size第 89 行。音效开关控制打开lottery.js定位到第 215 行const SOUND_ENABLED true; // 设为 false 可关闭音效避免会议室回声年会现场若麦克风未关音效会触发啸叫。建议提前设为false用外放音箱单独播音效。禁用右键与缩放index.html第 18 行meta标签已内置meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno此设置防止投屏时被误触缩放。若用 Chrome 投屏还需在地址栏右侧点击「三个点 → 更多工具 → 创建快捷方式」勾选「在窗口中打开」生成桌面快捷方式——这样双击启动的是独立窗口无地址栏干扰。3. 抽奖逻辑拆解Fisher-Yates 洗牌 localStorage 持久化为什么它比 Math.random() 直接取更公平很多人以为“随机抽”就是names[Math.floor(Math.random() * names.length)]但真正在 200 人规模下连续抽 30 次会出现明显分布偏差靠前/靠后名字中奖概率偏高中间段偏低。这个源码用的是经工业验证的Fisher-Yates 洗牌算法Knuth Shuffle配合localStorage实现状态快照确保每次抽奖都是“从未中奖池中真随机抽取”且断电不丢进度。3.1 Fisher-Yates 洗牌让每个位置被选中的概率严格等于 1/n核心代码在lottery.js第 132 行shuffleArray(array)函数function shuffleArray(array) { for (let i array.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); // 关键范围是 [0, i]不是 [0, array.length) [array[i], array[j]] [array[j], array[i]]; // ES6 解构交换 } return array; }逻辑说明从数组末尾开始i length-1每次随机选一个索引j范围0到i含i将i位置与j位置元素交换i自减继续处理前i-1个元素。这样做第i个位置被选中的概率 1/(i1)而它最终留在原位的概率 (1-1/(i1)) × (1-1/i) × ...数学推导可证每个元素落在任意位置的概率均为1/n。对比直觉写法若用Math.random() * array.length固定范围会导致越往后交换次数越少首尾元素“被扰动”机会不均等。3.2 localStorage 持久化不是存“已抽名单”而是存“剩余名单”很多同类源码把中奖者 push 进localStorage下次启动再过滤——这在多人协作时极易冲突A 抽了3个B 刷新页面看到的还是原始200人。本方案采用状态快照法初始化时将完整名单存入localStorage键lottery_remaining每次抽中一人从remaining数组中splice(index, 1)删除该人并覆盖写回localStorage页面加载时优先读lottery_remaining若为空则 fallback 到DEFAULT_NAMES。关键代码lottery.js第 285 行// 读取剩余名单 function loadRemainingNames() { const saved localStorage.getItem(lottery_remaining); return saved ? JSON.parse(saved) : [...DEFAULT_NAMES]; // 注意用扩展运算符深拷贝 } // 保存剩余名单抽完一人后调用 function saveRemainingNames(names) { localStorage.setItem(lottery_remaining, JSON.stringify(names)); }参数说明JSON.stringify()保证序列化安全避免函数/undefined 污染loadRemainingNames()中的[...DEFAULT_NAMES]是防御性写法——防止DEFAULT_NAMES被意外修改导致引用污染localStorage容量上限约 5MB200 个汉字姓名 ≈ 12KB完全无压力。3.3 防重复中奖与断电恢复两次校验 时间戳快照光靠remaining数组还不够。我们增加了双重保险抽中瞬间校验在drawWinner()函数中第 198 行执行if (!remainingNames.includes(winnerName)) { console.warn(⚠️ 警告${winnerName} 不在剩余名单中疑似 localStorage 被篡改); alert(检测到名单异常请刷新重试); return; }防止有人 F12 修改localStorage强行添加自己。启动时时间戳校验index.html第 35 行埋点script // 记录首次加载时间用于判断是否为全新启动 if (!localStorage.getItem(lottery_first_load)) { localStorage.setItem(lottery_first_load, Date.now().toString()); } /script后续可通过Date.now() - parseInt(localStorage.getItem(lottery_first_load))估算运行时长辅助判断是否需清空状态。4. 避坑指南6 个真实年会现场踩过的坑附现象、原因与秒解方案这些不是理论假设是我在 2023 年 12 月 22 日某互联网公司年会现场投影仪突然黑屏、音效消失、名单变乱码后用手机热点 tethering 连笔记本、开 Chrome DevTools 实时调试30 分钟内定位并修复的血泪经验。每一条都对应一个具体报错截图和解决命令。4.1 现象双击index.html打开空白页F12 看 Console 报错Uncaught SyntaxError: Unexpected token 原因你用 VS Code 或 Sublime Text 直接双击 HTML 文件但编辑器默认用file://协议打开而lottery.js中用了fetch(./data/names.csv)—— 浏览器出于安全策略禁止file://协议下 AJAX 请求本地文件返回的是 HTML 源码故解析出符号错误。解决✅ 正确做法用 Chrome 浏览器菜单栏 → 文件 → 打开文件选择index.html✅ 或用 Python 快速起一个本地 HTTP 服务无需安装# Python 3.x python -m http.server 8000 # 然后浏览器访问 http://localhost:8000❌ 禁止用 Edge/Firefox 双击打开它们对file://的限制更严❌ 禁止在 WebStorm 等 IDE 内置浏览器中打开仍走file://。4.2 现象CSV 导入后名单显示为 “张三,李四,王五”一行而非三人原因CSV 文件保存为ANSI 编码Windows 记事本默认而 JS 的FileReader.readAsText()默认按 UTF-8 解析导致换行符\r\n被当作文本字符。解决✅ 用记事本打开 CSV → 另存为 → 编码选UTF-8注意不是“UTF-8-BOM”BOM 会导致首行多出✅ 或用 VS Code右下角点击编码名如 ANSI→ 选择 “Reopen with Encoding” → UTF-8 → 再保存✅ 终极方案在lottery.js第 92 行parseCSV函数内加容错text text.replace(/\r\n/g, \n).replace(/\r/g, \n); // 统一换行符 return text.split(\n).map(n n.trim()).filter(Boolean);4.3 现象投屏时滚动动画卡顿中奖名字一闪而过原因Chrome 投屏默认启用硬件加速但老旧显卡如 Intel HD Graphics 4000驱动不兼容导致transform: translateY()动画掉帧。解决✅ 在style.css第 55 行.rolling类中将transition: transform 0.3s ease-out改为.rolling { transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); /* 更平滑的贝塞尔曲线 */ will-change: transform; /* 提示浏览器提前优化 */ }✅ 或彻底关闭硬件加速Chrome 地址栏输入chrome://settings/system→ 关闭 “使用硬件加速模式如果可用” → 重启浏览器。4.4 现象抽到第 5 人时页面弹出alert(localStorage 已满)原因某些企业版 Chrome 启用了策略组Group Policy限制localStorage容量为 2MB低于标准 5MB而你导入的 CSV 包含 500 人 详细工号如张三-00123序列化后超限。解决✅ 精简姓名字段只保留姓名删掉部门/工号抽奖只需识别领奖再核对✅ 清理无关localStorageF12 → Application → Storage → Local Storage → 点击file://→ 右键 Clear all✅ 替换持久化方案进阶将localStorage.setItem()替换为sessionStorage.setItem()牺牲断电恢复换容量宽松sessionStorage 无硬限制。4.5 现象点击“暂停”按钮无反应继续滚动原因lottery.js第 172 行isDrawing状态变量被多次setTimeout闭包捕获导致clearTimeout(drawTimer)无法清除正确的定时器 ID。解决✅ 在startRolling()函数开头强制重置drawTimer null✅ 在pauseRolling()函数中增加判空function pauseRolling() { if (drawTimer) { clearTimeout(drawTimer); drawTimer null; isDrawing false; document.getElementById(btn-pause).textContent ▶️ 继续; } }✅ 验证方法F12 → Console 输入drawTimer应返回null暂停后或数字 ID滚动中。4.6 现象中奖音效播放一次后后续不再响原因Chrome 70 对自动播放音频有严格策略必须由用户手势click/tap触发首个audio.play()之后才能静音播放。而本源码在drawWinner()中直接sound.play()第二次起被浏览器拦截。解决✅ 在initSound()函数第 205 行中将audio.play()移至按钮事件内document.getElementById(btn-draw).addEventListener(click, () { if (!soundPlayedOnce) { sound.play().catch(e console.log(首次播放需用户交互, e)); soundPlayedOnce true; } drawWinner(); });✅ 或更简单在index.html第 25 行audio标签加muted autoplay属性首次静音播放解锁权限后续再sound.muted false。5. 进阶技巧定制化改造三板斧——换皮肤、加倒计时、对接企业微信通知做到“能用”只是起点“好用”才是交付标准。下面这三个改造我在三家客户现场被当场追加需求平均耗时 ≤20 分钟且全部兼容原逻辑无需重构。5.1 换肤5 分钟切换蓝白科技风 / 红金喜庆风 / 黑金高端风源码默认是蓝白渐变style.css第 3 行--primary-color: #2563eb。换肤本质是批量替换 CSS 变量而非重写样式。风格主色变量辅色变量使用场景替换命令Linux/macOS红金喜庆风--primary-color: #dc2626酒红--accent-color: #fbbf24金--bg-color: #fef2f2浅粉春节年会、婚庆抽奖sed -i s/--primary-color: #2563eb;/--primary-color: #dc2626;/g style.css sed -i s/--accent-color: #3b82f6;/--accent-color: #fbbf24;/g style.css黑金高端风--primary-color: #111827炭黑--accent-color: #d97706古铜金--bg-color: #f9fafb浅灰高管晚宴、发布会sed -i s/--bg-color: #eff6ff;/--bg-color: #f9fafb;/g style.css sed -i s/--text-color: #1e40af;/--text-color: #111827;/g style.css操作步骤备份原style.csscp style.css style.css.bak执行对应sed命令Windows 用户用 Notepad 的“替换 → 全部替换”刷新页面检查按钮、滚动条、中奖框颜色是否统一若 Logo 不协调在assets/logo.png替换为同尺寸黑底/红底版本。5.2 加倒计时让“3、2、1 开始”成为仪式感锚点客户说“光滚动太单调要像春晚那样倒数完才启动。” 我们在lottery.js中注入一个轻量倒计时模块不依赖外部库。在startRolling()函数开头插入function startRolling() { // 新增倒计时逻辑 let countdown 3; const countdownEl document.getElementById(countdown); countdownEl.style.display block; // 显示倒计时容器 countdownEl.textContent countdown; const countdownTimer setInterval(() { countdown--; countdownEl.textContent countdown; if (countdown 0) { clearInterval(countdownTimer); countdownEl.style.display none; // 真正启动滚动 isDrawing true; drawTimer setTimeout(drawWinner, 100); } }, 1000); }并在index.html第 68 行/div前添加div idcountdown styledisplay:none; position:fixed; top:20%; left:50%; transform:translateX(-50%); font-size:80px; font-weight:bold; color:#dc2626; z-index:100;/div参数说明top:20%确保倒计时在投屏中央偏上避开主持人站位z-index:100保证覆盖滚动层1000ms间隔可改为800ms加快节奏若需语音播报可在此处加speechSynthesis.speak(new SpeechSynthesisUtterance(countdown.toString()))。5.3 对接企业微信通知抽中后自动推送中奖信息到个人账号这是最高频的定制需求。我们不接入复杂 API而是用企业微信「应用消息」的免鉴权方案生成一个带参数的跳转链接用户点击即打开企业微信并发送预设消息。步骤登录企业微信管理后台 → 应用管理 → 自建应用 → 复制「AgentId」和「Secret」在lottery.js第 250 行drawWinner()函数末尾添加function sendWeComNotice(name) { const corpId YOUR_CORP_ID; // 替换为你的真实 corpId const agentId YOUR_AGENT_ID; const redirectUrl encodeURIComponent(https://work.weixin.qq.com/api/send?msg${encodeURIComponent(恭喜 ${name} 获得三等奖请于今晚8点前到行政部领奖)}); const wecomUrl https://work.weixin.qq.com/wwopen/userAuth?corpid${corpId}agentid${agentId}redirect_uri${redirectUrl}; window.open(wecomUrl, _blank); } // 调用时机drawWinner() 最后一行 sendWeComNotice(winnerName);安全提示YOUR_CORP_ID和YOUR_AGENT_ID是公开信息不涉密redirect_uri必须是企业微信备案域名下的路径如https://yourcompany.com/wecom-callback此处用https://work.weixin.qq.com/api/send是伪地址实际需部署一个接收回调的轻量接口Python Flask 5 行代码即可若无备案域名退而求其次生成一个二维码图片用qrcode.js扫码后跳转企业微信聊天窗口预填文字。从那以后我每次交付年会抽奖系统都会在README.md末尾加一行# 部署前必做1. 检查 CSV 编码 2. 确认 Chrome 投屏模式 3. 备份原始 style.css——这三步做完剩下的就是调音效音量、试投屏角度、和主持人对流程。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →