ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HTML5年会抽奖源码:从公平抽奖算法到大屏视觉特效完整实现

HTML5年会抽奖源码:从公平抽奖算法到大屏视觉特效完整实现 简介html5公司年会抽奖源码面向企业行政、活动策划及前端开发者解决年会互动抽奖中员工名单配置繁琐、重复中奖、奖品临时调整难等痛点。代码支持动态配置员工信息并保证同一人只能中奖一次奖项既有内置的特等奖、一等奖、二等奖、三等奖也支持自定义奖项名称与等级界面样式、配色与抽奖音效均可灵活调整内置多套震撼音效直接运行index.html即可投入使用。压缩包共27个文件以png界面素材、mp3音效、js交互脚本和html入口为主另有css样式、mp4效果演示及readme说明包体约80.96MB按功能模块组织便于替换资源与维护代码。资源预设了主界面与中奖效果展示图便于快速预览现场视觉效果mp4演示还展示了抽奖流程与音效触发时机方便参考实际运行效果。目前已有2745人学习下载适合需要快速落地年会抽奖环节的团队直接使用也可基于源码二次开发定制接入自身员工数据或增加大屏互动特效。1. 年会抽奖为什么要选 HTML5一个单页能装下的现场方案办年会最怕的其实不是预算不够而是现场环节掉链子——尤其是抽奖。用纸质抽奖箱行政要抱着箱子满场跑领导喊停之后翻纸条全场都盯着你翻半天气氛瞬间凉一半。买商业抽奖软件几千块不说现场还得装驱动、调分辨率碰到会议室那台老掉牙的投影主机PPT 都卡更别说跑特效。HTML5 公司年会抽奖源码这个方向之所以这几年成了主流不是因为技术多玄学而是它正好卡在痛点上了一个 HTML 文件打开就能跑不需要装任何环境大屏和笔记本上都是同一个页面名单、规则、动效全在前端改起来比改 PPT 还快。我见过太多策划和行政被抽奖环节折腾到崩溃其实换一种实现思路就能解决。这个标题里的“最高端”我的理解不是特效堆得越多越好而是现场那种“所有人屏住呼吸、名字在大屏上滚动最后缓缓停住”的仪式感。这篇文章就把我从 0 搭一个年会抽奖页面的完整路径讲清楚规则怎么写才不会被质疑、名单怎么从 Excel 进到大屏、特效怎么做才不卡顿、以及最关键的——现场翻车了怎么救。如果你正准备接年会抽奖的活或者公司内部在选型这篇能帮你少走至少两周弯路。2. 抽奖核心逻辑先定不重复、可复核的规则再写代码2.1 为什么“公平”不是感觉问题而是算法问题年会抽奖跟抽盲盒不一样盲盒抽完就完了年会抽奖在现场几百双眼睛盯着中奖名单还要在大屏上停留好几分钟。任何一次“重复中奖”“名单对不上”“领导刚喊停那个人已经抽过”都会被放大成事故。所以写代码之前先别急着做界面先把抽奖规则定死。第一不重复原则。一等奖抽完的人绝对不能出现在二等奖的滚动名单里。这个在实现上不是靠“肉眼判断”而是要从抽奖池里直接剔除。第二可复核原则。现场抽完如果有人说“不公平”你得能证明这次抽奖是随机的而不是你提前排好的。很多商业软件回避这个问题但 HTML5 源码可以做到——用带种子的随机算法把“场次 轮次”作为种子抽完一轮记录一次事后用同一个种子重放一遍结果序列完全一致。有这个能力行政和老板都踏实。第三还要处理“作废与重抽”。现场一定会有突发情况领导喊停时名字滚动停了结果发现这人已经中过奖了或者临时增加一个名额又或者中奖的人不在现场。所以系统里必须支持“重抽当前名额”并且重抽不能破坏已中奖集合。把这三条定下来后面写代码就是顺着规则翻译成逻辑不会返工。2.2 用一个带种子的洗牌算法保证可复现JavaScript 自带的Math.random()是无种子的每次刷新结果都不一样而且无法复现对年会抽奖来说这就是个坑。我一般会自己实现一个伪随机数生成器网上最常用的叫 mulberry32代码短、速度快、生成序列质量足够。然后配合 Fisher-Yates 洗牌算法在一开始就把名单打乱。// mulberry32: 传入一个 32 位整数种子返回一个返回 [0,1) 随机数的函数 function mulberry32(seed) { let a seed 0; return function () { a | 0; a (a 0x6D2B79F5) | 0; let t Math.imul(a ^ (a 15), 1 | a); t (t Math.imul(t ^ (t 7), 61 | t)) ^ t; return ((t ^ (t 14)) 0) / 4294967296; }; } // Fisher-Yates 洗牌原地打乱传入的数组 function shuffleArray(arr, rand) { for (let i arr.length - 1; i 0; i--) { const j Math.floor(rand() * (i 1)); [arr[i], arr[j]] [arr[j], arr[i]]; } return arr; }逻辑说明mulberry32内部通过位运算生成一个伪随机序列同一个seed进去出来的随机数序列一模一样。shuffleArray是标准洗牌算法保证每个元素落在每个位置的概率相等。抽奖时把两份名单放进池子里一份是全部人员信息一份是洗牌后的抽取顺序。抽取时从洗牌后的数组头部依次取取完一个就从池子剔除一个。参数说明seed建议用当前场次编号 * 100 轮次编号这样的整数组合比如第 2 场第 3 轮就是 203这个数字会在控制台显示出来方便记录和复核。mulberry32的种子必须是 32 位整数字符串要提前转成整数或哈希一下不能直接传。2.3 抽奖池的“剔除”和“重抽”实现洗牌算法解决的是顺序问题但现场操作时需要的是一套状态管理。我建议用一个LotteryPool类把整件事封装起来否则写几个setInterval和数组操作跑到现场改需求时会很狼狈。class LotteryPool { constructor(allList, seed) { this.allList allList.slice(); // 全部人员用于展示可抽人数 this.wonList []; // 已中奖者全场不能重复 this.currentPick null; // 当前正在抽的名额 this.seed seed; // 记录种子用于复核 this.tmpPool shuffleArray(allList.slice(), mulberry32(seed)); } // 从临时池里取出下一个未中奖的人 pickNext() { // 跳过已中奖的人防止数据残留导致重复 while (this.tmpPool.length 0) { const candidate this.tmpPool.shift(); if (!this.wonList.find(p p.id candidate.id)) { this.currentPick candidate; return candidate; } } return null; // 池子空了 } // 确认当前名额中奖 confirm() { if (this.currentPick) { this.wonList.push(this.currentPick); this.currentPick null; } } // 作废当前名额重新抽 redo() { this.currentPick null; // 不放入 wonList直接忽略 } // 用同一个 seed 重新生成全序列用于事后核对 replay() { return shuffleArray(this.allList.slice(), mulberry32(this.seed)); } }逻辑说明pickNext()每次从洗好的临时池头部取人但会做一个过滤万一之前有数据残留也能保证不中重复。confirm()确认并入选中名单redo()直接把当前候选丢掉不进入已中奖列表相当于这次抽取作废。replay()是给“较真”的同事准备的——同一份名单、同一个种子重跑出来的抽取顺序一模一样。参数说明id字段在名单导入时生成用姓名 工号或者姓名 手机尾号做唯一标识千万不能用姓名做唯一键同一家公司同名的人太多了。我这里用find遍历是因为名单通常只有几百人性能没问题如果名单达到几千人就改成用Set存已中奖的id。2.4 名单去重与“轮次名额”的设置年会抽奖通常不是一次抽一个人而是“三等奖抽 10 人”“二等奖抽 5 人”。每一轮对应一个名单子集轮次之间互不影响。常见做法是给每轮定义一个配置对象轮次名称、抽取人数、是否允许已中奖者参与有的公司“阳光普照奖”就允许重复中奖。配置文件长这样const ROUNDS [ { name: 三等奖, count: 10, allowRepeat: false }, { name: 二等奖, count: 5, allowRepeat: false }, { name: 一等奖, count: 3, allowRepeat: false }, { name: 特等奖, count: 1, allowRepeat: false }, { name: 阳光普照, count: 999, allowRepeat: true }, // 人人有份 ];逻辑说明allowRepeat为false的轮次pickNext()会通过wonList做全局去重为true时直接忽略wonList过滤从全量名单里抽。这个配置项是血的教训换来的——有的公司老板会临时说“今年大家都辛苦了再抽一轮没中奖的都能参与”如果你的系统不支持重复抽取当场就尴尬了。名单去重我建议在前端导入时就做一遍而不是等到抽奖时才发现问题。去重逻辑不复杂按id建一个Map重复的保留第一个并弹窗提示“检测到 3 条重复数据已自动去重”。真正会出问题的地方在下一章——名单文件的编码和格式那才是现场最容易翻车的位置。3. 让现场“最高端”的视觉层粒子、3D 翻转与名字滚动的实现细节3.1 高端感的核心不是特效是“名字滚动的节奏”很多人以为高端就是多加特效粒子多一点、光效炫一点、声音大一点。但真在现场观众注意力只在一件事上大屏上的名字什么时候停下来。所以我建议把 80% 的精力放在“滚动减速”的观感上特效只是锦上添花。名字滚动我不用setInterval而是用requestAnimationFrame。原因很简单setInterval在浏览器标签页被遮挡时会降频甚至暂停而且无法精确控制每一帧的位移量滚动看起来会一顿一顿的。requestAnimationFrame跟随屏幕刷新率一般是 60fps每一帧回调一次我可以在回调里根据时间差计算位移从而实现“先快后慢再停”的缓动效果。let rafId null; let startTime 0; const DURATION 8000; // 减速动画总时长8 秒 const MAX_SPEED 300; // 每秒滚过的像素数 function rollNames(canvasCtx, yOffset, name, onDone) { cancelAnimationFrame(rafId); startTime performance.now(); function frame(now) { const elapsed now - startTime; const progress Math.min(elapsed / DURATION, 1); // smoothstep 缓动曲线让速度从快到慢平滑过渡 const eased progress * progress * (3 - 2 * progress); const currentSpeed MAX_SPEED * (1 - eased); yOffset currentSpeed / 60; drawName(canvasCtx, name, yOffset); // 下一帧继续滚动或达到时间阈值时停止 if (progress 1) { rafId requestAnimationFrame(frame); } else { onDone(); } } rafId requestAnimationFrame(frame); }逻辑说明performance.now()是相对页面打开的时间戳精度到毫秒能保证减速动画在不同性能的机器上耗时一致。progress从 0 到 1经过smoothstep曲线映射后eased在开头增长快、尾部增长慢视觉上就是先快速翻滚、最后缓缓停下。currentSpeed随eased线性下降配合名字列表的循环位移就形成了“彩票开奖”那种紧张感。参数说明DURATION建议设在 6 到 10 秒。太短了观众还没进入状态就停了太长了会被吐槽“怎么还没抽完”。MAX_SPEED大屏上建议 250 到 350小的笔记本屏幕可以降到 200否则会看到明显的闪烁感。3.2 粒子特效用 Canvas 2D别一上来就上 WebGL年会大屏最常见的硬件是会议室投影仪分辨率高但刷新率也就 30 到 60Hz而且老设备不少。为了一个抽奖页面引入 WebGL 或者 Three.js低端电脑上可能出现画面撕裂甚至黑屏。我的建议是粒子特效用 Canvas 2D 足够数量控制在 300 个以内GPU 开销极小还能保证 60 帧。class ParticleField { constructor(canvas) { this.canvas canvas; this.ctx canvas.getContext(2d); this.particles []; this.running false; } init(count 250) { this.particles Array.from({ length: count }, () ({ x: Math.random() * this.canvas.width, y: Math.random() * this.canvas.height, vx: (Math.random() - 0.5) * 0.8, vy: -Math.random() * 0.6 - 0.2, // 粒子缓慢向上飘 size: Math.random() * 2 1, alpha: Math.random() * 0.6 0.4, })); } tick() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); for (const p of this.particles) { p.x p.vx; p.y p.vy; if (p.y -10) { // 移出屏幕后重置到底部 p.y this.canvas.height 10; p.x Math.random() * this.canvas.width; } this.ctx.globalAlpha p.alpha; this.ctx.fillStyle #ffd700; this.ctx.beginPath(); this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); this.ctx.fill(); } if (this.running) requestAnimationFrame(() this.tick()); } start() { this.running true; this.tick(); } stop() { this.running false; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } }逻辑说明粒子对象只有x/y/vx/vy/size/alpha六个字段每帧只做一次坐标更新和圆形绘制CPU 占用非常低。粒子向上飘散、越过大屏后重置到底部形成持续的氛围感。金色#ffd700配合深色背景在现场投影上最出效果白色反而会被环境光吃掉。参数说明count是粒子总数建议根据设备调整——笔记本集成显卡跑 500 个也没事但投影仪的分辨率高Canvas 实际渲染像素多保守起见用 250。粒子的vx和vy控制在 ±1 像素/帧以内视觉上是“飘”而不是“飞”太快的运动会分散观众注意力。3.3 大屏自适应的布局用 vw 单位代替像素年会现场的大屏有两种可能会议室 16:9 投影幕布或者宴会厅 LED 屏。LED 屏的物理分辨率常常是 1920×1080 或更高但浏览器窗口并不一定是全屏状态。这里有一个很常见的坑用px写死字号在小窗口预览没问题投到大屏上字模糊、比例失调。我推荐的做法是外层容器宽度固定为100vw字体和间距全部用vw单位。比如主标题用font-size: 6vw人名用font-size: 8vw。这样不管窗口是 1080p 还是 4K 分辨率元素永远铺满屏幕比例不会变。注意控制台页面不要用这套布局控制台是给操作人员看的用普通像素布局反而更方便。/* 主屏样式适配任意分辨率大屏 */ html, body { margin: 0; padding: 0; height: 100%; background: #0a0e1a; overflow: hidden; } .screen-root { width: 100vw; height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; background: radial-gradient(ellipse at center, #1a2340 0%, #0a0e1a 85%); } .winner-name { font-size: 8vw; font-weight: 700; color: #ffd700; text-shadow: 0 0 30px rgba(255, 215, 0, 0.7); letter-spacing: 0.08em; font-family: PingFang SC, Microsoft YaHei, sans-serif; } .round-title { font-size: 3vw; color: rgba(255, 255, 255, 0.8); margin-bottom: 4vh; }参数说明8vw在 1920 宽屏幕上约等于 154 像素足够让宴会厅后排看清4K 屏上是 308 像素视觉比例一致。text-shadow的金色光晕能提升屏上字体的“发光感”但在投影仪上不要用太强的模糊值否则会显得字脏。overflow: hidden是必须的——万一某个名字过长宁可截断也不允许页面出现滚动条。4. 现场最常翻车的 5 个坑黑屏、卡顿、误抽、名单错乱、临时改规则4.1 名单导入乱码Excel 保存的 CSV 和浏览器编码不对付现象用 Excel 整理好员工名单另存为 CSV浏览器里读取后中文全部变成乱码屏幕上出现“锟斤拷”这类常见乱码字符。原因Excel 在中文 Windows 上保存 CSV 默认是 GBK/GB2312 编码而现代浏览器默认按 UTF-8 解码。编码不匹配时中文就会变成乱码。这跟代码本身没关系属于文件编码问题。解决最简单粗暴的办法是让用户改用.txt格式每行一条名单类似于“张三 技术部”。如果一定要支持 CSV就在代码里先尝试按 UTF-8 解码检测到乱码后用TextDecoder(gbk)重新解码。此外还可以用记事本打开 CSV另存为时选择 UTF-8 编码再导入就正常了。我在源码里会同时支持两种格式第一次读出来乱码会自动尝试 GBK 解码实在不行再提示用户转编码。4.2 抽到一半发现已中奖的人又出现在名单里现象抽三等奖抽了 5 个人抽二等奖时滚动名单里又出现了这 5 个名字中的一个。原因最常见的是“已中奖过滤”只做了前端展示过滤没有从抽取池里剔除或者名单有两个数据源一个是展示用的全量名单一个是抽取用的临时池两处不一致。还有一种是上一轮中奖后没有调用confirm()导致已中奖者一直停留在临时池里。解决把“已中奖者”的id在confirm()时就插入一个SetpickNext()每取一个候选人先查Set命中就跳过。展示层和抽取层必须共用同一个池子不能各维护一份名单。另外在控制台加一个“当前轮次已中奖名单”面板主持人每抽完一个人肉眼能核对一遍比靠代码自信靠谱。4.3 大屏上字体发虚、边缘锯齿明显现象页面在笔记本上清晰锐利投到大屏之后文字发虚金色边缘有锯齿像打了马赛克。原因两个因素叠加——第一用了transform: scale()整体缩放浏览器对缩放后的文字不做子像素渲染导致发虚第二LED 屏本身的物理像素排列不是标准的 RGB对细字体和高对比边缘天然不友好。解决不要用transform: scale()做适配用vw单位做真实布局让浏览器直接以目标分辨率渲染。字体上选择中粗或粗体字重避免使用极细的字体文字阴影保留但要控制模糊半径。还有一个技巧在控制台上把主屏浏览器按 F11 全屏并把 Windows 显示缩放到 100%——系统缩放如果设在 125%浏览器渲染出的实际像素会变清晰度直接打折。4.4 抽奖过程中“卡了一下”正好停在不该停的位置现象名字滚动到一半画面突然掉帧然后名字停住了全场都以为抽完了其实动画还没结束。原因requestAnimationFrame回调里如果还跑着粒子特效、音效播放、名单 DOM 操作同时进行某段时间 CPU 被打满帧率会掉。尤其老一代双核处理器配合集成显卡掉帧是必然的。解决把粒子数量和滚动动画拆到两个不同的帧循环里粒子可以接受 30fps但名字滚动必须保持 60fps。做法是检测帧间隔——如果两帧间隔超过 25ms说明掉帧下一帧自动跳过粒子绘制优先保证名字滚动的连续性。或者更简单抽奖时暂停粒子抽完并确认结果后再恢复粒子现场观感反而是“静下来”更显庄重。4.5 领导临时说“再加一个名额”代码当场改不了现象三等奖本来抽 10 人领导说“今年大家辛苦了加到 15 人吧”。如果逻辑写死count: 10控制台就没有入口增加名额现场只能重启页面重新抽前面 10 个人白抽了。原因轮次配置在代码里写死了没有做成运行时可调的状态。解决控制台界面加一个“本轮剩余名额”的加减按钮每确认一个中奖者剩余名额减一领导要求加人时操作员点“增加名额”系统继续从池子里抽已中奖记录保留不变。源码里把这个状态放到一个reactive对象里用Object.defineProperty或直接proxy加名额即时生效。这一点看着简单现场救过我好几次。5. 名单导入与双屏控制把控制台拿在手里主屏只留仪式感5.1 主屏与控制台分离的架构有些公司用一台电脑接两块屏有些用一台笔记本投大屏、操作也在笔记本上还有一种是我比较推荐的——一台笔记本连大屏操作员打开控制台页面和主屏页面通过浏览器本地通信联动。这样做的原因很现实操作员的桌面不能曝光上面有按钮、名单、抽奖状态被投到大屏上会很尴尬。HTML5 页面是静态文件没有后端所以主屏和控制台之间不能用 WebSocket 之类的东西。我一般用BroadcastChannel——这是现代浏览器都支持的原生 API专门解决同一浏览器不同标签页之间的通信问题。两个页面都部署在本地可以是file://协议也可以是局域网静态服务器控制台发消息主屏监听数据走浏览器内部不会暴露到公网。// 控制台页面发送操作指令 const channel new BroadcastChannel(lottery-channel); function sendCommand(command, payload) { channel.postMessage({ command, payload, timestamp: Date.now() }); } // 例点击“开始滚动” sendCommand(START_ROLL, { round: 二等奖 }); // 例点击“确认中奖”同时把中奖人信息发过去 sendCommand(CONFIRM_WINNER, { id: 2024-001, name: 张三, dept: 技术部 });// 主屏页面接收指令 const channel new BroadcastChannel(lottery-channel); channel.onmessage (event) { const { command, payload } event.data; switch (command) { case START_ROLL: startRollAnimation(payload.round); break; case CONFIRM_WINNER: showWinnerOnScreen(payload); break; case REDO: redoCurrentDraw(); break; default: break; } };逻辑说明BroadcastChannel的构造参数是频道名称同一个名称的页面之间才能互相通信。控制台发START_ROLL主屏启动滚动动画控制台发CONFIRM_WINNER主屏把中奖者姓名和部门以大字展示出来。timestamp字段用来防止消息乱序——如果操作员手速过快连点了两下主屏可以根据时间戳丢弃过期消息。参数说明file://协议下BroadcastChannel在 Chrome 和 Edge 中都能正常工作但 Firefox 对本地文件的限制更严格如果打不开就改用localStorage加storage事件的方式原理一样兼容性更好。现场建议提前用 Firefox 和 Chrome 各测一遍不要默认“浏览器都行”。5.2 名单导入支持 TXT、CSV 和 Excel 导出的几种格式控制台页面上放一个大文件拖拽区支持.txt、.csv。Excel 的.xlsx文件浏览器不能直接读但可以通过 Excel 另存为 CSV 或 TXT 来导入。代码里读取文件的逻辑很简单function parseRoster(file) { const reader new FileReader(); reader.onload (e) { const raw e.target.result; // 尝试按 UTF-8 解析失败则回退 GBK let text raw; if (raw.includes(\uFFFD)) { text new TextDecoder(gbk).decode(new Uint8Array(raw)); } // 按换行符拆分跳过空行 const lines text.split(/\r?\n/).map(l l.trim()).filter(Boolean); // 每条数据姓名 部门用逗号/制表符/空格分隔 const roster lines.map((line, idx) { const parts line.split(/[,\t]/); const name parts[0]?.trim(); const dept parts[1]?.trim() || 未知部门; return { id: ${name}_${idx}, name, dept }; }); console.log(导入成功共 ${roster.length} 人); }; reader.readAsText(file, utf-8); }逻辑说明先用 UTF-8 读如果文件是 GBK 编码readAsText会得到带\uFFFD替换字符的结果这时再用TextDecoder(gbk)重新解码。这里有个细节readAsText和TextDecoder的解码结果不完全一样所以最稳妥的方式是先读成ArrayBuffer再按 UTF-8 试解码有乱码就换 GBK。线上临时处理的话先按上面的逻辑跑能覆盖九成场景。参数说明id生成规则是姓名_行号因为同一家公司同名员工可能在隔了几行的地方出现用行号能区分如果你觉得用行号不够正规可以在导入时让操作员加一列工号用工号做id更严谨。部门字段必须解析因为大屏上显示“张三 技术部”比只显示名字更有仪式感很多名字相同的人加了部门就不容易混淆。5.3 控制台的 5 个核心按钮控制台界面不需要好看但按钮的位置必须闭着眼睛都能摸到。我按现场操作顺序设计导入名单文件拖拽或点击选择导入后右侧显示“共 N 人去重 M 人”。开始滚动主屏启动名字滚动此时按钮变为“停止滚动”。停止滚动主持人喊停后点击主屏减速停止并显示当前候选人。确认中奖候选人显示后点击将该人锁定为中奖者主屏定格放大。重抽当前候选人有问题时点击该候选人作废重新滚动。按钮之间要用明显状态锁开始滚动后“导入名单”和“确认中奖”必须不可点防止操作员误触。这个锁看上去毫不起眼但在现场紧张气氛下误触的概率比你想象的高——有一年我亲眼见到操作员想点“确认”结果点到了“重抽”当场把已经中奖的人刷掉了全场哗然。所以源码里每个按钮的disabled状态都要随着流程切换。5.4 主屏上的辅助信息轮次、剩余名额、背景音乐主屏除了显示大号名字底部还要有辅助信息区当前轮次“二等奖”、剩余名额“剩余 3/5 人”字小一点不影响主视觉。轮次和剩余名额来自控制台推送的消息控制台每确认一个中奖者就广播一次。背景音乐这一块我建议在控制台页面上放一个音频播放器由操作员手动控制而不是在主屏上用autoplay自动播放。原因很现实浏览器的自动播放策略会拦截没有用户交互的声音主屏页面一打开就被静音的情况太常见了。控制台上操作员至少会点一次按钮这个点击事件就能解锁音频播放。而且背景音乐的音量要压到正常语音的 70%因为现场音箱全靠它出氛围声音太大主持人说话都被盖住了反而显得不专业。6. 结果可验证与现场节奏控制行政放心、老板不尴尬的收尾技巧6.1 用同一份种子重放抽奖过程年会结束之后行政往往需要留档。如果只是保存一个中奖名单 Excel万一有人质疑“这个名单是不是提前排好的”行政没法回答。我建议源码里做一个“复核模式”控制台页面有个按钮叫“生成抽奖报告”点击后输出一段 JSON内容包括场次代号、每轮的种子值、中奖名单、以及对应的抽取序列。事后打开页面输入场次代号系统会用同样的种子重新跑一遍逐轮展示“当时应该是谁中奖”和现场记录下来的一比对完全一致就说明没有人为干预。这个能力不用展示给全公司看但行政和老板需要的时候你能给出一份可复核的说明信任度会提高很多。实际使用时要注意种子要当场记录抽完一轮就把它显示在控制台上方便操作员随手拍照存证。{ eventId: 2025-annual-03, seedBase: 203, rounds: [ { roundName: 三等奖, seed: 203, winnerIds: [张伟_12, 李娜_45, 王强_78] } ] }参数说明seed的生成规则是eventId的哈希加上轮次编号也可以用字符串拼接后做一个简单哈希保证不同场次、不同轮次的序列完全不同。注意不要把已中奖名单之外的内部状态写进报告——报告中只需要种子、名单、结果三者即可。6.2 现场节奏抽奖环节控制在 15 分钟以内控制抽奖节奏比写代码还难。年会流程里抽奖通常被安排在节目之间现场观众喝了酒注意力涣散一旦滚动动画磨蹭超过 15 秒底下就开始聊天。我的做法是给每个轮次设置一个最大时长提示三等奖 10 人控制在 4 到 5 分钟一等奖 1 人控制在 40 秒以内。这里有个经验公式滚动动画 6 秒停顿时主持人念单位和姓名 8 秒下台领奖上台合影 20 秒每人约 35 秒。10 个人就是 350 秒接近 6 分钟正好在观众耐心临界点之前。所以源码里我在控制台做了一个“节奏计时器”每轮开始后自动倒计时显示剩余时间超时黄色闪烁。这不是给观众看的是给操作员和主持人做提醒的。倒计时期间操作员可以根据主持人的语速决定什么时候按“停止滚动”不用慌慌张张。6.3 我最看重的一个小技巧给主屏加一个“空屏”按钮现场最尴尬的一刻往往不是抽奖本身而是抽奖结束之后——主持人宣布“让我们恭喜以上同事”但大屏还停留着最后一个人的名字金色光效闪啊闪全场都在等你关掉它。有的操作员手忙脚乱去关页面桌面图标全亮了。我的解决办法在控制台加一个“空屏专用键”按一下主屏切换到暗色背景加“请欣赏下一个节目”的占位图再按一下可以回到抽奖页面。这个动作不需要关闭页面不会暴露控制台桌面也不会因为重新打开页面导致名单状态丢失。实际操作上我一般建议年会开始前把主屏浏览器开成全屏然后控制台页面留在副显示器或笔记本桌面整个过程主屏页面从打开到结束都不关、不刷新状态只会往前走不会倒退。6.4 抽奖前必须做的 5 分钟检查单源码写得再好现场环境变了还是会翻车。我给自己定的规矩是年会开始前一小时到会场花 5 分钟走一遍这个检查单——名单乱码、投影分辨率、电脑缩放、浏览器全屏存储权限、声音输出走对设备。具体做法是连上大屏后先导入名单确认中文正常显示用浏览器 F12 控制台跑一次模拟抽奖确认动画流畅、名单无重复把音响播到 50% 音量试听背景乐最后把主屏页面刷新一次确认联系人状态还在。还有一个容易被忽略的点把笔记本的“省电模式”关掉电源插上。年会现场灯光暗、投影仪开着MacBook 或 Windows 笔记本如果等会儿自动锁屏或休眠那才是最便宜的翻车方式。自动锁屏把抽奖页面晾在一边全场安静等你解锁这场景我见过一次就再也不敢忘。这些年做下来我的最大感受是抽奖页面最后拼的不是技术炫不炫而是对现场流程的理解。那些“最高端”的效果图远不如一个不会在中场掉链子的系统实在。希望这篇笔记能帮你避掉我踩过的那些坑让年会的抽奖环节成为全场最顺畅、也最被记住的高光时刻。本文还有配套的精品资源点击获取
返回列表