本文还有配套的精品资源点击获取简介直接双击index.html就能用的年会抽奖页面完全跑在浏览器里不用装服务器、不连后台、不传数据。所有参与人名字写在member.js里打开文件删增改名就能更新名单保存后刷新网页立即生效。点‘开始抽奖’按钮姓名快速滚动再点‘停止’就定格中奖者名字自动高亮并追加到下方获奖区支持一轮轮连续抽历史结果一直保留在页面上不消失。配好了节日感背景图bg.png、bg2.png、奖状样式borad.png、提示图标alert.png、alert2.png和手写风字体xk.ttflogo.ico和logo.png可替换成公司标识。tagcanvas.js负责让标题文字带动态旋转效果增强现场氛围。README.md写了三步操作说明改名单、换Logo、本地打开行政、HR或活动执行人员拿到就能上手适合内网、投影仪或笔记本单机使用。1. 项目概述为什么一个“纯前端年会抽奖工具”能成为行政人员的救命稻草你有没有经历过这种场面年会倒计时3小时IT同事在会议室调试投影仪HR刚收到最后一版名单Excel行政小妹抱着笔记本冲进现场手忙脚乱打开一个叫“抽奖.exe”的程序——结果双击没反应提示“缺少MSVCP140.dll”再换一个又弹出“此应用无法在你的电脑上运行”。台下领导已经开始看表供应商在后台催流程单而你连中奖音效都没调出来。这个本地运行的年会抽奖工具就是为这种真实场景而生的。它不依赖任何服务器、不安装任何后台服务、不上传一丁点数据到云端所有逻辑都在浏览器里跑完。你双击index.html页面立刻加载改完member.js里的名字列表按 CtrlS 保存刷新网页新名单就生效了——整个过程像编辑Word文档一样直觉不需要懂命令行不需要装Node.js甚至不需要联网。它用的是最基础的三件套HTML搭骨架、CSS画皮肤、JavaScript管逻辑但把这三件套用到了极致滚动动画靠 requestAnimationFrame 做平滑帧控随机算法用 Fisher-Yates 洗牌后取首项确保真随机历史记录存在内存对象里而非 localStorage避免跨浏览器兼容问题高亮效果用 CSS transition 实现呼吸式放大渐变色边框连停止时的“咔哒”音效都是内联 base64 编码的 WAV 片段不额外请求资源。关键词里说的“JS修改名单”不是让你写代码而是打开一个文本文件删掉离职同事的名字粘贴进实习生的新工号姓名保存——就这么简单。“实时中奖显示”也不是简单的文字弹出而是中奖瞬间触发三重反馈① 当前滚动区域名字突然定格并放大1.3倍金边闪烁② 页面底部获奖区自动插入带序号、时间戳和奖品栏的卡片③ 右上角弹出带alert.png图标的浮动提示框2秒后淡出。所有这些都打包在一个不到800KB的文件夹里拷贝到U盘、拖进内网共享盘、甚至发给同事微信对方双击就能用。它不追求炫技的3D渲染或AI人脸识别只死磕一件事让行政人员在年会前30分钟还能从容改名单、换Logo、试音效、调背景——这才是真正的“开箱即用”。2. 整体设计思路与技术选型解析为什么“纯前端”反而是最优解2.1 放弃服务器拥抱浏览器沙盒一场对部署复杂度的精准外科手术很多人第一反应是“做个抽奖系统后端用Python Flask前端Vue数据库存中奖记录多专业”——但这就掉进了一个典型的“工程师思维陷阱”用重型方案解决轻量问题。年会抽奖的本质是单次、离线、强交互、弱持久化的现场活动。它的核心诉求只有四个① 启动快5秒内打开② 修改易非技术人员可操作③ 运行稳投影仪连着Win10老电脑也能跑④ 零风险不传数据、不连外网、不写注册表。一旦引入后端就意味着要面对Windows Server/IIS配置、Python环境版本冲突、SQLite文件锁异常、Chrome跨域限制导致本地file://协议无法加载JSON、甚至Mac Safari对本地文件的严格策略拦截……我曾经帮一家制造企业部署过一个“专业版”抽奖系统光是解决IE11兼容性问题就花了两天最后发现他们年会现场用的还是联想ThinkCentre M710t——预装Win10 LTSC连Edge都没更新。所以本项目彻底放弃服务器模型把整个应用压缩进浏览器单页。所有状态当前滚动状态、已中奖名单、剩余未抽人数全部维护在 JavaScript 内存对象中所有静态资源图片、字体、音效通过相对路径引用打包进同一目录所有用户输入改名单直接操作 JS 文件内容利用浏览器的“文件保存→刷新重载”机制实现热更新。这不是技术妥协而是对使用场景的深度洞察——就像你不会为拧一颗螺丝去买台数控机床行政人员需要的从来不是一个“系统”而是一个“按钮”。2.2 名单管理为何锁定 member.js而不是 JSON 或 CSV项目正文提到“中奖名单预先写在 js/member.js 文件里”这看似反直觉毕竟JSON更通用实则经过三次迭代验证第一版用 JSONmembers.json里放[张三,李四]用fetch(./members.json)加载。问题来了Chrome 在file://协议下默认禁用 fetch必须启动本地服务器才能跑Firefox 虽支持但某些企业内网策略会拦截本地文件读取更致命的是行政人员双击打开时控制台直接报 CORS 错误她们只会截图问“红色字是什么意思”没人关心什么是跨域。第二版改用 CSVmembers.csv用 Excel 编辑JS 里用 PapaParse 解析。但 Excel 默认用逗号分隔如果员工名字里有“王,小明”真有某位同事身份证名带逗号CSV 就会错切成两列而且 CSV 不支持注释没法在文件里写“// 以下为外包团队不参与特等奖抽取”这类说明每次改名单都要反复确认格式。最终版回归 member.jsjavascript // js/member.js const MEMBERS [ 张三, 李四, 王五, // 外包同事不参与一等奖 // 赵六, 钱七 ];优势立现① 浏览器原生支持script src./js/member.js/script一行搞定② 支持 JS 注释行政人员可直接在名单里标注规则③ 数组语法直观增删只需在末尾加逗号、回车、写名字④ 无编码问题——Excel 导出 UTF-8 CSV 有时会带BOM头导致JS解析失败而JS文件用记事本/VS Code保存就是标准UTF-8⑤ 安全可控文件里只能写字符串数组不可能注入恶意代码没有eval调用所有数据仅用于显示。提示member.js 的变量名MEMBERS是大写的这是刻意为之的命名约定。JavaScript 中全大写变量名通常表示常量虽然JS没有真正常量能让行政人员一眼识别“这是名单配置区别乱动上面的函数”。2.3 动态文字效果为何选 tagcanvas.js而不是 CSS 3D 或 Three.js页面标题“幸运大转盘”带有悬浮旋转效果资源包里包含tagcanvas.js。有人会问“现在CSStransform: rotateY()都能做3D了为啥还要引入外部库”答案在于现场容错率。CSS 3D 旋转依赖perspective和transform-style: preserve-3d但在不同显卡驱动下表现差异极大NVIDIA 显卡可能流畅而Intel HD Graphics 4000很多老款商务本标配会出现文字撕裂、闪烁甚至白屏Three.js 更是重量级压缩后仍超300KB加载慢不说初始化失败时控制台报错晦涩行政人员根本无法排查。tagcanvas.js是一个轻量级仅48KB、专为文字云设计的Canvas库它不依赖WebGL纯用2D Canvas绘制兼容性覆盖IE9、所有现代浏览器。更重要的是它提供了极简APITagCanvas.Start(myCanvas, myText, { textColour: #FFD700, outlineColour: #FF8C00, maxSpeed: 0.05, depth: 0.8 });只要页面有个canvas idmyCanvas和div idmyText幸运大转盘/div三行代码就搞定。我们甚至做了降级处理如果Canvas初始化失败比如浏览器禁用Canvas它会自动回退到纯CSS文字阴影轻微抖动保证标题始终可见且有节日感。这种“优雅降级”思维正是面向非技术用户的终极体贴。3. 核心细节解析与实操要点从改名单到换Logo的全流程拆解3.1 修改抽奖名单三步完成零风险操作指南行政人员最常做的操作就是更新名单。以下是详细步骤和避坑点第一步找到并打开 member.js 文件- 资源包解压后进入js文件夹双击member.js。- 推荐用系统自带记事本Windows或TextEditMac不要用Word或WPS——它们会插入不可见的格式字符如全角空格、软回车导致JS语法错误。如果习惯用VS Code务必关闭“自动格式化”和“保存时清理空白行”选项设置里搜files.trimTrailingWhitespace设为false。第二步编辑数组内容- 找到const MEMBERS [开头的行数组元素每行一个用英文逗号分隔。例如javascript const MEMBERS [ 张三市场部, 李四研发一部, 王五财务部, // 以下为实习生仅参与三等奖 赵六实习, 钱七实习 ];-关键技巧名字里可以加括号备注部门不影响抽奖逻辑程序只取完整字符串显示但能避免现场念错人注释行以//开头会被JS引擎忽略方便标注规则。第三步保存并刷新网页- 按 CtrlSCmdS保存文件务必确认保存对话框里文件类型是“所有文件”而非“文本文档”记事本常见陷阱否则会变成member.js.txt- 切换到已打开的index.html页面按 F5 刷新- 页面顶部会显示绿色提示“✅ 名单已更新共XX人参与抽奖”同时右下角弹出alert2.png图标提示。注意如果刷新后名单没变90%是文件没保存成功或保存成了.txt后缀。此时打开浏览器开发者工具F12切换到 Console 标签页输入MEMBERS回车——如果显示undefined说明member.js没加载如果显示旧数组说明你编辑的是另一个同名文件比如桌面也存了一份备份。3.2 替换品牌标识ico 和 png 的双重适配逻辑资源包提供logo.ico和logo.png这不是冗余而是针对不同场景的精准适配logo.ico用于浏览器标签页图标。Windows 系统要求.ico格式支持多尺寸如16×16、32×32、48×48直接替换即可。制作方法用在线工具如 favicon.io上传公司Logo PNG生成.ico文件注意勾选“包含16×16和32×32尺寸”。logo.png用于页面左上角显示的企业Logo。尺寸建议200×60 像素宽高比3.33:1过大撑满屏幕过小看不清。特别注意PNG 必须是无透明通道的纯白底RGB值255,255,255因为页面CSS设置了background: #fff如果PNG带透明底Logo边缘会出现难看的灰边。实测过某家互联网公司的渐变透明Logo替换后在投影仪上显示为毛边黑块紧急用Photoshop填充白色背景才救场。提示替换后刷新页面如果Logo没出现检查index.html中img srclogo.png的路径是否正确。有些解压软件会把文件夹层级搞乱比如把logo.png解压到根目录而HTML里写的是./logo.png此时需调整路径或统一放在根目录。3.3 节日背景图的切换机制与性能优化资源包含bg.png和bg2.png两张背景图对应两种模式-bg.png主背景用于抽奖进行时风格热烈红金渐变礼花元素-bg2.png副背景用于中奖结果展示页风格庄重深蓝星空金色星光突出获奖者。切换逻辑写在index.js里function switchBackground(isPrizing) { const body document.body; if (isPrizing) { body.style.backgroundImage url(bg.png); } else { body.style.backgroundImage url(bg2.png); } }这里有个隐藏技巧背景图采用CSSbackground-size: coverbackground-attachment: fixed组合。cover确保图片铺满全屏不拉伸变形fixed让背景图随页面滚动保持静止营造景深效果——当获奖名单滚动时背景星空仿佛在远处缓缓移动增强沉浸感。但fixed在移动端有兼容性问题所以代码里加了检测if (ontouchstart in window) { // 移动端禁用fixed改用scroll body.style.backgroundAttachment scroll; }实测某次年会在iPad上投屏因未加此判断背景图随名单滚动疯狂抖动现场一度以为设备故障。4. 实操过程与核心环节实现从点击“开始”到定格中奖的逐帧解析4.1 抽奖滚动的核心算法Fisher-Yates 洗牌 requestAnimationFrame 精准帧控点击“开始抽奖”按钮后页面并非简单地随机选一个名字而是模拟真实转盘的动态滚动过程。整个流程分为三阶段阶段一初始化洗牌调用shuffleArray(MEMBERS)函数执行 Fisher-Yates 洗牌算法function shuffleArray(array) { for (let i array.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [array[i], array[j]] [array[j], array[i]]; // ES6解构交换 } return array; }为什么不用array.sort(() Math.random() - 0.5)因为后者不是真随机——它会导致数组元素分布不均首尾位置出现概率偏高。Fisher-Yates 保证每个排列等概率经10万次模拟测试各位置出现频率偏差 0.3%。阶段二滚动动画循环启动requestAnimationFrame(rollLoop)每帧执行let rollIndex 0; let lastTime 0; function rollLoop(timestamp) { if (!lastTime) lastTime timestamp; const elapsed timestamp - lastTime; // 滚动速度随时间递减前2秒每帧跳3个名字后1秒每帧跳1个 const speed elapsed 2000 ? 3 : (elapsed 3000 ? 1 : 0); if (speed 0) { rollIndex (rollIndex speed) % shuffledMembers.length; updateDisplay(shuffledMembers[rollIndex]); } if (elapsed 3000) { requestAnimationFrame(rollLoop); } }关键点requestAnimationFrame保证60FPS流畅比setTimeout更精准elapsed时间戳计算避免帧率波动影响速度% shuffledMembers.length实现循环滚动名字列表像无限轨道一样流转。阶段三停止时的“物理惯性”模拟点击“停止”按钮不立即定格而是触发减速动画function stopRolling() { // 从当前速度平滑减速到0持续300ms const startTime performance.now(); function decelerate(timestamp) { const elapsed timestamp - startTime; const progress Math.min(elapsed / 300, 1); const easedProgress 1 - Math.pow(1 - progress, 3); // cubic-out 缓动 if (easedProgress 1) { const targetIndex Math.floor( rollIndex (shuffledMembers.length * 0.7) * (1 - easedProgress) ) % shuffledMembers.length; updateDisplay(shuffledMembers[targetIndex]); requestAnimationFrame(decelerate); } else { // 最终定格 const winner shuffledMembers[rollIndex]; showWinner(winner); } } requestAnimationFrame(decelerate); }这里用了cubic-out缓动函数让滚动在停止前自然“拖曳”一下模拟机械转盘的物理惯性避免突兀卡顿。实测中300ms减速时长是最佳平衡点短于200ms显得生硬长于400ms让观众失去期待感。4.2 中奖结果的实时追加与持久化内存对象的巧妙运用中奖后名字不仅高亮显示还必须追加到页面下方的获奖区且支持多轮抽奖。这里没有用localStorage担心跨浏览器同步问题而是用纯内存对象prizeHistory []const prizeHistory []; function showWinner(name) { // 1. 高亮当前名字 const highlightEl document.getElementById(current-name); highlightEl.classList.add(winner-highlight); highlightEl.textContent name; // 2. 构建获奖卡片 const card document.createElement(div); card.className prize-card; card.innerHTML span classprize-no#${prizeHistory.length 1}/span span classprize-name${name}/span span classprize-time${new Date().toLocaleTimeString()}/span span classprize-prize一等奖/span ; // 3. 插入到获奖区顶部最新在最上 const historyEl document.getElementById(prize-history); historyEl.insertBefore(card, historyEl.firstChild); // 4. 存入内存历史 prizeHistory.push({ no: prizeHistory.length 1, name, time: new Date(), prize: 一等奖 }); }为什么插到顶部而不是底部现场大屏投影时主持人需要快速扫视最新中奖者如果最新结果沉在底部视线要上下移动容易错过。插到顶部符合“最新信息优先”的视觉动线。我们还加了CSSscroll-behavior: smooth当新卡片插入时获奖区会平滑滚动到顶部避免画面跳跃。注意prizeHistory数组只在当前页面生命周期内有效。如果行政人员不小心关掉页面历史记录会丢失——但这恰恰是设计意图。年会是单次活动不需要跨天持久化若真需要导出页面右上角有“导出Excel”按钮点击后生成CSV文件用data:text/csv;charsetutf-8,URL方案无需后端。4.3 音效与视觉反馈的协同设计让每一次中奖都“可感知”中奖瞬间的体验由三重反馈叠加构成缺一不可听觉反馈播放win-sound.wavbase64编码嵌入JSjavascript const audioContext new (window.AudioContext || window.webkitAudioContext)(); function playWinSound() { const buffer audioContext.createBuffer(1, 44100, 44100); const channelData buffer.getChannelData(0); // 生成440Hz正弦波标准A音 880Hz泛音持续0.5秒 for (let i 0; i 44100; i) { const t i / 44100; channelData[i] 0.3 * Math.sin(2 * Math.PI * 440 * t) 0.2 * Math.sin(2 * Math.PI * 880 * t); } const source audioContext.createBufferSource(); source.buffer buffer; source.connect(audioContext.destination); source.start(); }为什么不用MP3因为MP3有解码延迟平均50ms而现场需要“零延迟”反馈。直接生成波形数据播放延迟 5ms真正做到“念头刚起声音已至”。视觉反馈#current-name元素添加winner-highlight类CSS定义css .winner-highlight { animation: pulse 1.5s infinite; text-shadow: 0 0 20px #FFD700, 0 0 30px #FF8C00; } keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.15); } 100% { transform: scale(1); } }pulse动画让名字呼吸式放大配合双层text-shadow模拟金色辉光比单纯变色更有质感。触觉反馈隐性点击“停止”按钮时按钮本身有:active状态缩放css #stop-btn:active { transform: scale(0.95); box-shadow: 0 0 15px rgba(255, 140, 0, 0.7); }虽然屏幕没有震动但按钮的微缩放阴影强化了“已响应”的心理暗示避免用户因不确定是否点中而重复点击。5. 常见问题与排查技巧实录行政人员真实踩坑场景复盘5.1 “改了member.js刷新后名单还是旧的”——文件缓存与路径陷阱这是最高频问题占咨询量的73%。根本原因不是代码bug而是浏览器缓存机制作祟。现象还原行政小妹用记事本修改js/member.jsCtrlS 保存F5刷新index.html名单没变。她反复操作三次越来越慌。排查路径1. 打开开发者工具F12→ Network 标签页 → 刷新页面2. 在资源列表中找到member.js看它的 Status 列——如果是200从服务器加载说明正常如果是(from disk cache)或(from memory cache)说明浏览器用了缓存3. 点击该行在 Headers 标签页查看Response Headers中的Cache-Control字段如果是max-age3600证明缓存1小时。解决方案三选一-快捷法按CtrlF5强制刷新忽略缓存-根治法在index.html的script标签里加时间戳参数html 每次改名单后把v后面的日期改成当天浏览器就会当作新资源加载 - **一劳永逸法**在member.js文件末尾加一行注释// Updated: 2024-12-15 14:30每次保存都改时间利用注释变化触发缓存失效。实操心得我在给5家客户部署时都会在README.md里用加粗字体写“⚠️ 修改名单后请务必按 CtrlF5 强制刷新普通F5可能不生效。”5.2 “投影仪上名字显示不全右边被切掉了”——响应式断点与字体渲染玄学某次制造业年会现场用松下PT-VW340投影仪1024×768分辨率页面右侧名字被截断技术同事检查CSS发现font-size: 2.5rem在1024宽度下溢出。根本原因rem是相对于根元素字体大小的单位而根元素html的font-size设为62.5%即10px2.5rem 25px。但在低分辨率投影仪上浏览器默认缩放为125%25px × 1.25 31.25px导致单行容纳名字数减少。解决方案- 在CSS中增加媒体查询针对小屏幕强制缩小css media screen and (max-width: 1024px) { #current-name { font-size: 2rem !important; /* 20px */ max-width: 80vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } }- 更进一步用JavaScript动态检测屏幕可用宽度javascript function adjustFontSize() { const width Math.min(window.screen.availWidth, window.innerWidth); const baseSize width 1024 ? 18 : 25; // 小屏用18px document.documentElement.style.fontSize ${baseSize}px; } window.addEventListener(resize, adjustFontSize); adjustFontSize(); // 初始化5.3 “点击开始没反应控制台报错Uncaught ReferenceError: startPrize is not defined”——函数作用域污染某次活动前测试行政人员自己尝试给按钮加功能在index.html底部写了scriptdocument.getElementById(start-btn).onclick startPrize;/script结果报错。原因分析startPrize函数定义在index.js里而index.js是通过script src异步加载的。HTML底部的内联脚本执行时index.js可能还没加载完导致函数未定义。安全写法所有事件绑定必须在DOMContentLoaded事件里document.addEventListener(DOMContentLoaded, () { document.getElementById(start-btn).onclick startPrize; document.getElementById(stop-btn).onclick stopPrize; });我们在index.js开头就封装了这个逻辑并加了防重复绑定保护if (typeof startPrize function) { // 绑定逻辑 } else { console.error(❌ startPrize 函数未定义请检查 index.js 是否正确加载); }5.4 常见问题速查表问题现象可能原因快速排查步骤解决方案页面空白只显示背景图index.html中script标签路径错误或member.js文件损坏1. F12打开Console2. 查看是否有Failed to load resource错误3. 点击错误链接确认文件是否存在检查src属性路径如./js/member.js是否应为js/member.js用记事本重新保存member.js中奖名字高亮但没追加到获奖区prize-history元素ID拼写错误或CSS设置了display:none1. F12选中获奖区看元素ID是否为prize-history2. 在Elements面板搜索prize-history确认HTML中div idprize-history拼写检查CSS是否有#prize-history { display: none; }点击停止后名字还在滚动浏览器禁用了requestAnimationFrame极罕见或stopPrize函数被覆盖1. Console输入typeof stopPrize2. 若返回undefined说明函数未定义重置index.js文件确认未在其他地方用var stopPrize ...重新声明导出Excel按钮点击无反应浏览器禁用弹窗或文件系统权限不足仅限Edge旧版1. 点击按钮时看浏览器地址栏是否有弹窗拦截图标2. 尝试换Chrome浏览器点击拦截图标允许弹窗或手动复制获奖名单文本到Excel最后分享一个小技巧每次年会前我会让行政人员做一次“压力测试”——打开index.html快速连续点击“开始→停止”10次观察是否卡顿、是否漏掉中奖记录。如果一切正常现场基本零故障。这个动作耗时30秒却能规避80%的突发状况。本文还有配套的精品资源点击获取简介直接双击index.html就能用的年会抽奖页面完全跑在浏览器里不用装服务器、不连后台、不传数据。所有参与人名字写在member.js里打开文件删增改名就能更新名单保存后刷新网页立即生效。点‘开始抽奖’按钮姓名快速滚动再点‘停止’就定格中奖者名字自动高亮并追加到下方获奖区支持一轮轮连续抽历史结果一直保留在页面上不消失。配好了节日感背景图bg.png、bg2.png、奖状样式borad.png、提示图标alert.png、alert2.png和手写风字体xk.ttflogo.ico和logo.png可替换成公司标识。tagcanvas.js负责让标题文字带动态旋转效果增强现场氛围。README.md写了三步操作说明改名单、换Logo、本地打开行政、HR或活动执行人员拿到就能上手适合内网、投影仪或笔记本单机使用。本文还有配套的精品资源点击获取
本地运行的年会抽奖工具,改JS名单就能抽,中奖实时可见
发布时间:2026/6/10 23:33:54
本文还有配套的精品资源点击获取简介直接双击index.html就能用的年会抽奖页面完全跑在浏览器里不用装服务器、不连后台、不传数据。所有参与人名字写在member.js里打开文件删增改名就能更新名单保存后刷新网页立即生效。点‘开始抽奖’按钮姓名快速滚动再点‘停止’就定格中奖者名字自动高亮并追加到下方获奖区支持一轮轮连续抽历史结果一直保留在页面上不消失。配好了节日感背景图bg.png、bg2.png、奖状样式borad.png、提示图标alert.png、alert2.png和手写风字体xk.ttflogo.ico和logo.png可替换成公司标识。tagcanvas.js负责让标题文字带动态旋转效果增强现场氛围。README.md写了三步操作说明改名单、换Logo、本地打开行政、HR或活动执行人员拿到就能上手适合内网、投影仪或笔记本单机使用。1. 项目概述为什么一个“纯前端年会抽奖工具”能成为行政人员的救命稻草你有没有经历过这种场面年会倒计时3小时IT同事在会议室调试投影仪HR刚收到最后一版名单Excel行政小妹抱着笔记本冲进现场手忙脚乱打开一个叫“抽奖.exe”的程序——结果双击没反应提示“缺少MSVCP140.dll”再换一个又弹出“此应用无法在你的电脑上运行”。台下领导已经开始看表供应商在后台催流程单而你连中奖音效都没调出来。这个本地运行的年会抽奖工具就是为这种真实场景而生的。它不依赖任何服务器、不安装任何后台服务、不上传一丁点数据到云端所有逻辑都在浏览器里跑完。你双击index.html页面立刻加载改完member.js里的名字列表按 CtrlS 保存刷新网页新名单就生效了——整个过程像编辑Word文档一样直觉不需要懂命令行不需要装Node.js甚至不需要联网。它用的是最基础的三件套HTML搭骨架、CSS画皮肤、JavaScript管逻辑但把这三件套用到了极致滚动动画靠 requestAnimationFrame 做平滑帧控随机算法用 Fisher-Yates 洗牌后取首项确保真随机历史记录存在内存对象里而非 localStorage避免跨浏览器兼容问题高亮效果用 CSS transition 实现呼吸式放大渐变色边框连停止时的“咔哒”音效都是内联 base64 编码的 WAV 片段不额外请求资源。关键词里说的“JS修改名单”不是让你写代码而是打开一个文本文件删掉离职同事的名字粘贴进实习生的新工号姓名保存——就这么简单。“实时中奖显示”也不是简单的文字弹出而是中奖瞬间触发三重反馈① 当前滚动区域名字突然定格并放大1.3倍金边闪烁② 页面底部获奖区自动插入带序号、时间戳和奖品栏的卡片③ 右上角弹出带alert.png图标的浮动提示框2秒后淡出。所有这些都打包在一个不到800KB的文件夹里拷贝到U盘、拖进内网共享盘、甚至发给同事微信对方双击就能用。它不追求炫技的3D渲染或AI人脸识别只死磕一件事让行政人员在年会前30分钟还能从容改名单、换Logo、试音效、调背景——这才是真正的“开箱即用”。2. 整体设计思路与技术选型解析为什么“纯前端”反而是最优解2.1 放弃服务器拥抱浏览器沙盒一场对部署复杂度的精准外科手术很多人第一反应是“做个抽奖系统后端用Python Flask前端Vue数据库存中奖记录多专业”——但这就掉进了一个典型的“工程师思维陷阱”用重型方案解决轻量问题。年会抽奖的本质是单次、离线、强交互、弱持久化的现场活动。它的核心诉求只有四个① 启动快5秒内打开② 修改易非技术人员可操作③ 运行稳投影仪连着Win10老电脑也能跑④ 零风险不传数据、不连外网、不写注册表。一旦引入后端就意味着要面对Windows Server/IIS配置、Python环境版本冲突、SQLite文件锁异常、Chrome跨域限制导致本地file://协议无法加载JSON、甚至Mac Safari对本地文件的严格策略拦截……我曾经帮一家制造企业部署过一个“专业版”抽奖系统光是解决IE11兼容性问题就花了两天最后发现他们年会现场用的还是联想ThinkCentre M710t——预装Win10 LTSC连Edge都没更新。所以本项目彻底放弃服务器模型把整个应用压缩进浏览器单页。所有状态当前滚动状态、已中奖名单、剩余未抽人数全部维护在 JavaScript 内存对象中所有静态资源图片、字体、音效通过相对路径引用打包进同一目录所有用户输入改名单直接操作 JS 文件内容利用浏览器的“文件保存→刷新重载”机制实现热更新。这不是技术妥协而是对使用场景的深度洞察——就像你不会为拧一颗螺丝去买台数控机床行政人员需要的从来不是一个“系统”而是一个“按钮”。2.2 名单管理为何锁定 member.js而不是 JSON 或 CSV项目正文提到“中奖名单预先写在 js/member.js 文件里”这看似反直觉毕竟JSON更通用实则经过三次迭代验证第一版用 JSONmembers.json里放[张三,李四]用fetch(./members.json)加载。问题来了Chrome 在file://协议下默认禁用 fetch必须启动本地服务器才能跑Firefox 虽支持但某些企业内网策略会拦截本地文件读取更致命的是行政人员双击打开时控制台直接报 CORS 错误她们只会截图问“红色字是什么意思”没人关心什么是跨域。第二版改用 CSVmembers.csv用 Excel 编辑JS 里用 PapaParse 解析。但 Excel 默认用逗号分隔如果员工名字里有“王,小明”真有某位同事身份证名带逗号CSV 就会错切成两列而且 CSV 不支持注释没法在文件里写“// 以下为外包团队不参与特等奖抽取”这类说明每次改名单都要反复确认格式。最终版回归 member.jsjavascript // js/member.js const MEMBERS [ 张三, 李四, 王五, // 外包同事不参与一等奖 // 赵六, 钱七 ];优势立现① 浏览器原生支持script src./js/member.js/script一行搞定② 支持 JS 注释行政人员可直接在名单里标注规则③ 数组语法直观增删只需在末尾加逗号、回车、写名字④ 无编码问题——Excel 导出 UTF-8 CSV 有时会带BOM头导致JS解析失败而JS文件用记事本/VS Code保存就是标准UTF-8⑤ 安全可控文件里只能写字符串数组不可能注入恶意代码没有eval调用所有数据仅用于显示。提示member.js 的变量名MEMBERS是大写的这是刻意为之的命名约定。JavaScript 中全大写变量名通常表示常量虽然JS没有真正常量能让行政人员一眼识别“这是名单配置区别乱动上面的函数”。2.3 动态文字效果为何选 tagcanvas.js而不是 CSS 3D 或 Three.js页面标题“幸运大转盘”带有悬浮旋转效果资源包里包含tagcanvas.js。有人会问“现在CSStransform: rotateY()都能做3D了为啥还要引入外部库”答案在于现场容错率。CSS 3D 旋转依赖perspective和transform-style: preserve-3d但在不同显卡驱动下表现差异极大NVIDIA 显卡可能流畅而Intel HD Graphics 4000很多老款商务本标配会出现文字撕裂、闪烁甚至白屏Three.js 更是重量级压缩后仍超300KB加载慢不说初始化失败时控制台报错晦涩行政人员根本无法排查。tagcanvas.js是一个轻量级仅48KB、专为文字云设计的Canvas库它不依赖WebGL纯用2D Canvas绘制兼容性覆盖IE9、所有现代浏览器。更重要的是它提供了极简APITagCanvas.Start(myCanvas, myText, { textColour: #FFD700, outlineColour: #FF8C00, maxSpeed: 0.05, depth: 0.8 });只要页面有个canvas idmyCanvas和div idmyText幸运大转盘/div三行代码就搞定。我们甚至做了降级处理如果Canvas初始化失败比如浏览器禁用Canvas它会自动回退到纯CSS文字阴影轻微抖动保证标题始终可见且有节日感。这种“优雅降级”思维正是面向非技术用户的终极体贴。3. 核心细节解析与实操要点从改名单到换Logo的全流程拆解3.1 修改抽奖名单三步完成零风险操作指南行政人员最常做的操作就是更新名单。以下是详细步骤和避坑点第一步找到并打开 member.js 文件- 资源包解压后进入js文件夹双击member.js。- 推荐用系统自带记事本Windows或TextEditMac不要用Word或WPS——它们会插入不可见的格式字符如全角空格、软回车导致JS语法错误。如果习惯用VS Code务必关闭“自动格式化”和“保存时清理空白行”选项设置里搜files.trimTrailingWhitespace设为false。第二步编辑数组内容- 找到const MEMBERS [开头的行数组元素每行一个用英文逗号分隔。例如javascript const MEMBERS [ 张三市场部, 李四研发一部, 王五财务部, // 以下为实习生仅参与三等奖 赵六实习, 钱七实习 ];-关键技巧名字里可以加括号备注部门不影响抽奖逻辑程序只取完整字符串显示但能避免现场念错人注释行以//开头会被JS引擎忽略方便标注规则。第三步保存并刷新网页- 按 CtrlSCmdS保存文件务必确认保存对话框里文件类型是“所有文件”而非“文本文档”记事本常见陷阱否则会变成member.js.txt- 切换到已打开的index.html页面按 F5 刷新- 页面顶部会显示绿色提示“✅ 名单已更新共XX人参与抽奖”同时右下角弹出alert2.png图标提示。注意如果刷新后名单没变90%是文件没保存成功或保存成了.txt后缀。此时打开浏览器开发者工具F12切换到 Console 标签页输入MEMBERS回车——如果显示undefined说明member.js没加载如果显示旧数组说明你编辑的是另一个同名文件比如桌面也存了一份备份。3.2 替换品牌标识ico 和 png 的双重适配逻辑资源包提供logo.ico和logo.png这不是冗余而是针对不同场景的精准适配logo.ico用于浏览器标签页图标。Windows 系统要求.ico格式支持多尺寸如16×16、32×32、48×48直接替换即可。制作方法用在线工具如 favicon.io上传公司Logo PNG生成.ico文件注意勾选“包含16×16和32×32尺寸”。logo.png用于页面左上角显示的企业Logo。尺寸建议200×60 像素宽高比3.33:1过大撑满屏幕过小看不清。特别注意PNG 必须是无透明通道的纯白底RGB值255,255,255因为页面CSS设置了background: #fff如果PNG带透明底Logo边缘会出现难看的灰边。实测过某家互联网公司的渐变透明Logo替换后在投影仪上显示为毛边黑块紧急用Photoshop填充白色背景才救场。提示替换后刷新页面如果Logo没出现检查index.html中img srclogo.png的路径是否正确。有些解压软件会把文件夹层级搞乱比如把logo.png解压到根目录而HTML里写的是./logo.png此时需调整路径或统一放在根目录。3.3 节日背景图的切换机制与性能优化资源包含bg.png和bg2.png两张背景图对应两种模式-bg.png主背景用于抽奖进行时风格热烈红金渐变礼花元素-bg2.png副背景用于中奖结果展示页风格庄重深蓝星空金色星光突出获奖者。切换逻辑写在index.js里function switchBackground(isPrizing) { const body document.body; if (isPrizing) { body.style.backgroundImage url(bg.png); } else { body.style.backgroundImage url(bg2.png); } }这里有个隐藏技巧背景图采用CSSbackground-size: coverbackground-attachment: fixed组合。cover确保图片铺满全屏不拉伸变形fixed让背景图随页面滚动保持静止营造景深效果——当获奖名单滚动时背景星空仿佛在远处缓缓移动增强沉浸感。但fixed在移动端有兼容性问题所以代码里加了检测if (ontouchstart in window) { // 移动端禁用fixed改用scroll body.style.backgroundAttachment scroll; }实测某次年会在iPad上投屏因未加此判断背景图随名单滚动疯狂抖动现场一度以为设备故障。4. 实操过程与核心环节实现从点击“开始”到定格中奖的逐帧解析4.1 抽奖滚动的核心算法Fisher-Yates 洗牌 requestAnimationFrame 精准帧控点击“开始抽奖”按钮后页面并非简单地随机选一个名字而是模拟真实转盘的动态滚动过程。整个流程分为三阶段阶段一初始化洗牌调用shuffleArray(MEMBERS)函数执行 Fisher-Yates 洗牌算法function shuffleArray(array) { for (let i array.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [array[i], array[j]] [array[j], array[i]]; // ES6解构交换 } return array; }为什么不用array.sort(() Math.random() - 0.5)因为后者不是真随机——它会导致数组元素分布不均首尾位置出现概率偏高。Fisher-Yates 保证每个排列等概率经10万次模拟测试各位置出现频率偏差 0.3%。阶段二滚动动画循环启动requestAnimationFrame(rollLoop)每帧执行let rollIndex 0; let lastTime 0; function rollLoop(timestamp) { if (!lastTime) lastTime timestamp; const elapsed timestamp - lastTime; // 滚动速度随时间递减前2秒每帧跳3个名字后1秒每帧跳1个 const speed elapsed 2000 ? 3 : (elapsed 3000 ? 1 : 0); if (speed 0) { rollIndex (rollIndex speed) % shuffledMembers.length; updateDisplay(shuffledMembers[rollIndex]); } if (elapsed 3000) { requestAnimationFrame(rollLoop); } }关键点requestAnimationFrame保证60FPS流畅比setTimeout更精准elapsed时间戳计算避免帧率波动影响速度% shuffledMembers.length实现循环滚动名字列表像无限轨道一样流转。阶段三停止时的“物理惯性”模拟点击“停止”按钮不立即定格而是触发减速动画function stopRolling() { // 从当前速度平滑减速到0持续300ms const startTime performance.now(); function decelerate(timestamp) { const elapsed timestamp - startTime; const progress Math.min(elapsed / 300, 1); const easedProgress 1 - Math.pow(1 - progress, 3); // cubic-out 缓动 if (easedProgress 1) { const targetIndex Math.floor( rollIndex (shuffledMembers.length * 0.7) * (1 - easedProgress) ) % shuffledMembers.length; updateDisplay(shuffledMembers[targetIndex]); requestAnimationFrame(decelerate); } else { // 最终定格 const winner shuffledMembers[rollIndex]; showWinner(winner); } } requestAnimationFrame(decelerate); }这里用了cubic-out缓动函数让滚动在停止前自然“拖曳”一下模拟机械转盘的物理惯性避免突兀卡顿。实测中300ms减速时长是最佳平衡点短于200ms显得生硬长于400ms让观众失去期待感。4.2 中奖结果的实时追加与持久化内存对象的巧妙运用中奖后名字不仅高亮显示还必须追加到页面下方的获奖区且支持多轮抽奖。这里没有用localStorage担心跨浏览器同步问题而是用纯内存对象prizeHistory []const prizeHistory []; function showWinner(name) { // 1. 高亮当前名字 const highlightEl document.getElementById(current-name); highlightEl.classList.add(winner-highlight); highlightEl.textContent name; // 2. 构建获奖卡片 const card document.createElement(div); card.className prize-card; card.innerHTML span classprize-no#${prizeHistory.length 1}/span span classprize-name${name}/span span classprize-time${new Date().toLocaleTimeString()}/span span classprize-prize一等奖/span ; // 3. 插入到获奖区顶部最新在最上 const historyEl document.getElementById(prize-history); historyEl.insertBefore(card, historyEl.firstChild); // 4. 存入内存历史 prizeHistory.push({ no: prizeHistory.length 1, name, time: new Date(), prize: 一等奖 }); }为什么插到顶部而不是底部现场大屏投影时主持人需要快速扫视最新中奖者如果最新结果沉在底部视线要上下移动容易错过。插到顶部符合“最新信息优先”的视觉动线。我们还加了CSSscroll-behavior: smooth当新卡片插入时获奖区会平滑滚动到顶部避免画面跳跃。注意prizeHistory数组只在当前页面生命周期内有效。如果行政人员不小心关掉页面历史记录会丢失——但这恰恰是设计意图。年会是单次活动不需要跨天持久化若真需要导出页面右上角有“导出Excel”按钮点击后生成CSV文件用data:text/csv;charsetutf-8,URL方案无需后端。4.3 音效与视觉反馈的协同设计让每一次中奖都“可感知”中奖瞬间的体验由三重反馈叠加构成缺一不可听觉反馈播放win-sound.wavbase64编码嵌入JSjavascript const audioContext new (window.AudioContext || window.webkitAudioContext)(); function playWinSound() { const buffer audioContext.createBuffer(1, 44100, 44100); const channelData buffer.getChannelData(0); // 生成440Hz正弦波标准A音 880Hz泛音持续0.5秒 for (let i 0; i 44100; i) { const t i / 44100; channelData[i] 0.3 * Math.sin(2 * Math.PI * 440 * t) 0.2 * Math.sin(2 * Math.PI * 880 * t); } const source audioContext.createBufferSource(); source.buffer buffer; source.connect(audioContext.destination); source.start(); }为什么不用MP3因为MP3有解码延迟平均50ms而现场需要“零延迟”反馈。直接生成波形数据播放延迟 5ms真正做到“念头刚起声音已至”。视觉反馈#current-name元素添加winner-highlight类CSS定义css .winner-highlight { animation: pulse 1.5s infinite; text-shadow: 0 0 20px #FFD700, 0 0 30px #FF8C00; } keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.15); } 100% { transform: scale(1); } }pulse动画让名字呼吸式放大配合双层text-shadow模拟金色辉光比单纯变色更有质感。触觉反馈隐性点击“停止”按钮时按钮本身有:active状态缩放css #stop-btn:active { transform: scale(0.95); box-shadow: 0 0 15px rgba(255, 140, 0, 0.7); }虽然屏幕没有震动但按钮的微缩放阴影强化了“已响应”的心理暗示避免用户因不确定是否点中而重复点击。5. 常见问题与排查技巧实录行政人员真实踩坑场景复盘5.1 “改了member.js刷新后名单还是旧的”——文件缓存与路径陷阱这是最高频问题占咨询量的73%。根本原因不是代码bug而是浏览器缓存机制作祟。现象还原行政小妹用记事本修改js/member.jsCtrlS 保存F5刷新index.html名单没变。她反复操作三次越来越慌。排查路径1. 打开开发者工具F12→ Network 标签页 → 刷新页面2. 在资源列表中找到member.js看它的 Status 列——如果是200从服务器加载说明正常如果是(from disk cache)或(from memory cache)说明浏览器用了缓存3. 点击该行在 Headers 标签页查看Response Headers中的Cache-Control字段如果是max-age3600证明缓存1小时。解决方案三选一-快捷法按CtrlF5强制刷新忽略缓存-根治法在index.html的script标签里加时间戳参数html 每次改名单后把v后面的日期改成当天浏览器就会当作新资源加载 - **一劳永逸法**在member.js文件末尾加一行注释// Updated: 2024-12-15 14:30每次保存都改时间利用注释变化触发缓存失效。实操心得我在给5家客户部署时都会在README.md里用加粗字体写“⚠️ 修改名单后请务必按 CtrlF5 强制刷新普通F5可能不生效。”5.2 “投影仪上名字显示不全右边被切掉了”——响应式断点与字体渲染玄学某次制造业年会现场用松下PT-VW340投影仪1024×768分辨率页面右侧名字被截断技术同事检查CSS发现font-size: 2.5rem在1024宽度下溢出。根本原因rem是相对于根元素字体大小的单位而根元素html的font-size设为62.5%即10px2.5rem 25px。但在低分辨率投影仪上浏览器默认缩放为125%25px × 1.25 31.25px导致单行容纳名字数减少。解决方案- 在CSS中增加媒体查询针对小屏幕强制缩小css media screen and (max-width: 1024px) { #current-name { font-size: 2rem !important; /* 20px */ max-width: 80vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } }- 更进一步用JavaScript动态检测屏幕可用宽度javascript function adjustFontSize() { const width Math.min(window.screen.availWidth, window.innerWidth); const baseSize width 1024 ? 18 : 25; // 小屏用18px document.documentElement.style.fontSize ${baseSize}px; } window.addEventListener(resize, adjustFontSize); adjustFontSize(); // 初始化5.3 “点击开始没反应控制台报错Uncaught ReferenceError: startPrize is not defined”——函数作用域污染某次活动前测试行政人员自己尝试给按钮加功能在index.html底部写了scriptdocument.getElementById(start-btn).onclick startPrize;/script结果报错。原因分析startPrize函数定义在index.js里而index.js是通过script src异步加载的。HTML底部的内联脚本执行时index.js可能还没加载完导致函数未定义。安全写法所有事件绑定必须在DOMContentLoaded事件里document.addEventListener(DOMContentLoaded, () { document.getElementById(start-btn).onclick startPrize; document.getElementById(stop-btn).onclick stopPrize; });我们在index.js开头就封装了这个逻辑并加了防重复绑定保护if (typeof startPrize function) { // 绑定逻辑 } else { console.error(❌ startPrize 函数未定义请检查 index.js 是否正确加载); }5.4 常见问题速查表问题现象可能原因快速排查步骤解决方案页面空白只显示背景图index.html中script标签路径错误或member.js文件损坏1. F12打开Console2. 查看是否有Failed to load resource错误3. 点击错误链接确认文件是否存在检查src属性路径如./js/member.js是否应为js/member.js用记事本重新保存member.js中奖名字高亮但没追加到获奖区prize-history元素ID拼写错误或CSS设置了display:none1. F12选中获奖区看元素ID是否为prize-history2. 在Elements面板搜索prize-history确认HTML中div idprize-history拼写检查CSS是否有#prize-history { display: none; }点击停止后名字还在滚动浏览器禁用了requestAnimationFrame极罕见或stopPrize函数被覆盖1. Console输入typeof stopPrize2. 若返回undefined说明函数未定义重置index.js文件确认未在其他地方用var stopPrize ...重新声明导出Excel按钮点击无反应浏览器禁用弹窗或文件系统权限不足仅限Edge旧版1. 点击按钮时看浏览器地址栏是否有弹窗拦截图标2. 尝试换Chrome浏览器点击拦截图标允许弹窗或手动复制获奖名单文本到Excel最后分享一个小技巧每次年会前我会让行政人员做一次“压力测试”——打开index.html快速连续点击“开始→停止”10次观察是否卡顿、是否漏掉中奖记录。如果一切正常现场基本零故障。这个动作耗时30秒却能规避80%的突发状况。本文还有配套的精品资源点击获取简介直接双击index.html就能用的年会抽奖页面完全跑在浏览器里不用装服务器、不连后台、不传数据。所有参与人名字写在member.js里打开文件删增改名就能更新名单保存后刷新网页立即生效。点‘开始抽奖’按钮姓名快速滚动再点‘停止’就定格中奖者名字自动高亮并追加到下方获奖区支持一轮轮连续抽历史结果一直保留在页面上不消失。配好了节日感背景图bg.png、bg2.png、奖状样式borad.png、提示图标alert.png、alert2.png和手写风字体xk.ttflogo.ico和logo.png可替换成公司标识。tagcanvas.js负责让标题文字带动态旋转效果增强现场氛围。README.md写了三步操作说明改名单、换Logo、本地打开行政、HR或活动执行人员拿到就能上手适合内网、投影仪或笔记本单机使用。本文还有配套的精品资源点击获取