news 2026/11/1 1:37:05

纯HTML年会抽奖源码:零依赖大屏滚动抽奖实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯HTML年会抽奖源码:零依赖大屏滚动抽奖实现

简介:这是一套面向公司年会、团建活动场景的前端抽奖源码,基于HTML与jQuery实现,适合有一定前端基础、希望快速搭建现场互动环节的开发者使用。它解决了传统抽奖工具样式单一、员工信息与奖品难以灵活配置的问题,支持动态录入员工名单、奖品自定义、中奖后自动排除重复中奖,并提供多种震撼音效与可调整的视觉样式,开箱即可运行。资源包共20个文件,包含html、js、css核心代码,以及mp4背景视频、mp3音效、png与jpg图片素材和psd设计源文件,压缩包约34.12MB,素材与逻辑分离,便于二次修改。目前已有6869人学习下载,读者可直接获得完整可运行的抽奖页面、音效与视觉素材、员工与奖品配置逻辑,以及样式调整的参考结构,快速落地一场有氛围感的年会抽奖环节。

1. 年会抽奖页面的真实需求:为什么我最后选了纯 HTML 方案

年底前两周,行政同事甩过来一个需求:公司年会现场大屏抽奖,要酷炫、要能滚动名字、要能一键出结果,最好还能临时改奖项。我第一反应是找个现成的抽奖系统,结果翻了一圈,要么是后端依赖一堆环境,要么是界面土得掉渣,要么改个奖项名称还得进数据库。最后我干脆用纯 HTML + CSS + JavaScript 撸了一个单文件版本,双击就能跑,U 盘拷到现场电脑上直接开。

这份源码解决的核心问题就三个:零环境依赖、现场可改、视觉能撑住大屏。它适合谁?适合没有专职前端、年会前三天才想起来要做抽奖页面的行政或技术同学,也适合想拿一个完整 DOM 操作 + 动画案例练手的前端新手。整个项目不涉及任何后端接口,所有名单和奖项都写在页面里,改起来就是改几行数组的事。下面我按「结构怎么搭 → 动画怎么做 → 名单怎么配 → 现场怎么救」的顺序,把这份源码拆开讲清楚。

2. 页面结构与视觉层:三层 DOM 怎么撑起大屏效果

2.1 抽奖页面的最小 DOM 骨架

很多人写抽奖页面,一上来就堆 div,最后自己都找不到哪个是哪个。我一般会把结构压到三层:背景层、滚动层、控制层。背景层负责氛围,滚动层负责名字滚动,控制层放按钮和奖项切换。这样后面改样式或者加动画,定位非常清晰。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>年会抽奖</title> <link rel="stylesheet" href="style.css"> </head> <body> <!-- 背景层:粒子/光效,纯装饰 --> <div class="bg-layer"></div> <!-- 滚动层:显示当前滚动的名字 --> <div class="roll-layer"> <div class="name-display" id="nameDisplay">准备开始</div> </div> <!-- 控制层:奖项切换 + 开始/停止 --> <div class="control-layer"> <div class="prize-tabs" id="prizeTabs"></div> <button class="btn-start" id="btnStart">开始抽奖</button> </div> <script src="main.js"></script> </body> </html>

这段结构里,bg-layer不参与任何逻辑,只做视觉;roll-layer里只有一个name-display,所有滚动文字都通过改它的textContent实现;control-layer里的prizeTabs是动态生成的,奖项数量变了也不用改 HTML。参数上唯一要注意的是viewport,现场大屏一般是 1920×1080,但笔记本调试时可能是 1366×768,所以字号必须用vw或vmin单位,不能写死px。

2.2 用 CSS 变量控制大屏配色和字号

现场最怕什么?投影偏色。你在笔记本上看是金色,投到幕布上变成土黄。所以配色不要写死,用 CSS 变量集中管理,现场花两分钟就能调。

:root { --bg-color: #0a0e27; --main-color: #ffd700; --accent-color: #ff6b6b; --name-size: 8vmin; --btn-size: 2.5vmin; } body { margin: 0; background: var(--bg-color); overflow: hidden; font-family: "Microsoft YaHei", sans-serif; } .name-display { font-size: var(--name-size); color: var(--main-color); text-align: center; line-height: 100vh; text-shadow: 0 0 20px var(--main-color); transition: transform 0.1s; } .btn-start { font-size: var(--btn-size); padding: 1em 3em; background: var(--accent-color); color: #fff; border: none; border-radius: 0.5em; cursor: pointer; }

--name-size: 8vmin是关键,vmin取视口宽高中较小的那个,保证不管横屏竖屏名字都不会溢出。text-shadow给金色名字加光晕,大屏上远看才有质感。transition: transform 0.1s是给后面滚动时的缩放抖动留的接口,不加的话名字切换会很生硬。

2.3 奖项切换标签的动态生成

奖项数量经常临时变,比如原本设了三等奖 10 个,老板说改成 15 个,又加了个特等奖。如果奖项写死在 HTML 里,改起来容易漏。我一般把奖项配置放在 JS 数组里,页面加载时动态生成标签。

const prizes = [ { name: "特等奖", count: 1 }, { name: "一等奖", count: 3 }, { name: "二等奖", count: 5 }, { name: "三等奖", count: 10 } ]; const tabsContainer = document.getElementById("prizeTabs"); prizes.forEach((prize, index) => { const tab = document.createElement("button"); tab.className = "prize-tab"; tab.textContent = `${prize.name}(${prize.count}人)`; tab.dataset.index = index; tab.addEventListener("click", () => switchPrize(index)); tabsContainer.appendChild(tab); }); let currentPrizeIndex = 0; function switchPrize(index) { currentPrizeIndex = index; document.querySelectorAll(".prize-tab").forEach((tab, i) => { tab.classList.toggle("active", i === index); }); document.getElementById("nameDisplay").textContent = prizes[index].name; }

prizes数组里count字段目前只用于显示,实际抽奖人数控制放在后面的抽奖逻辑里。dataset.index把索引挂在 DOM 上,点击时直接取,比用闭包或者indexOf更稳。switchPrize里先切active类,再把中间显示区改成奖项名,这样切换奖项时用户能立刻看到当前在抽哪个奖。

3. 滚动动画与抽奖逻辑:从 setInterval 到 requestAnimationFrame

3.1 名字滚动的两种实现方式对比

抽奖最核心的视觉就是名字快速滚动。新手最容易想到setInterval,每 50 毫秒换一个名字。但setInterval有个坑:如果页面切到后台,浏览器会降频,回来之后名字会卡一下。更稳的是requestAnimationFrame,它跟着屏幕刷新率走,大屏上更顺滑。

let rollTimer = null; let isRolling = false; function startRoll() { if (isRolling) return; isRolling = true; const display = document.getElementById("nameDisplay"); const names = getCurrentNames(); // 获取当前奖项的候选名单 function roll() { const randomIndex = Math.floor(Math.random() * names.length); display.textContent = names[randomIndex]; display.style.transform = `scale(${1 + Math.random() * 0.05})`; rollTimer = requestAnimationFrame(roll); } roll(); } function stopRoll() { if (!isRolling) return; isRolling = false; cancelAnimationFrame(rollTimer); // 最终结果由抽奖逻辑决定,这里只停止滚动 }

requestAnimationFrame每帧执行一次,60Hz 屏幕就是约 16.7 毫秒换一次名字,视觉上足够快。scale的随机抖动让名字有「跳动感」,但幅度控制在 5% 以内,太大反而晕。cancelAnimationFrame必须传对 id,否则停不下来,这是血泪经验——我第一次写的时候传了rollTimer但没赋值,结果按钮点了没反应。

3.2 抽奖结果去重与剩余人数控制

滚动只是表演,真正要保证的是:同一个人不能中两次,每个奖项抽够人数就停。我一般用两个数组:allNames存全部名单,remainingNames存还没中奖的人。每次抽奖从remainingNames里随机取一个,取完就splice掉。

const allNames = ["张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十"]; let remainingNames = [...allNames]; const winners = []; // 记录已中奖的人 function drawOne() { if (remainingNames.length === 0) { alert("所有人都已中奖,无法继续抽取"); return null; } const randomIndex = Math.floor(Math.random() * remainingNames.length); const winner = remainingNames[randomIndex]; remainingNames.splice(randomIndex, 1); winners.push(winner); return winner; }

remainingNames用扩展运算符复制,避免直接改allNames。splice的第二个参数是 1,表示删除一个元素。winners数组目前只记录,实际项目中可以渲染到侧边栏展示已中奖名单。注意:如果年会允许一个人中多个奖,那就不需要splice,直接每次从allNames里随机取就行,但这种情况很少见,默认还是去重。

3.3 停止滚动与结果揭晓的衔接

点「停止」按钮时,不能直接停,要有一个「减速 → 定格 → 高亮」的过程,否则太突兀。我一般用setTimeout做三段式:先减速滚动 500 毫秒,再定格显示结果,最后加一个放大动画。

function stopAndReveal() { if (!isRolling) return; isRolling = false; cancelAnimationFrame(rollTimer); const display = document.getElementById("nameDisplay"); const winner = drawOne(); if (!winner) return; // 定格结果 display.textContent = winner; display.style.transform = "scale(1.2)"; display.style.color = "#ff6b6b"; // 500ms 后恢复 setTimeout(() => { display.style.transform = "scale(1)"; display.style.color = "var(--main-color)"; }, 500); }

drawOne返回null时直接return,避免把null显示出来。scale(1.2)和颜色变化持续 500 毫秒,给现场一个「中奖了」的视觉反馈。这里有个细节:display.style.color直接写死了#ff6b6b,其实应该用 CSS 变量,但当时赶时间没改,后来发现现场投影下这个红色偏暗,又临时调成了#ff4757。所以建议一开始就用变量。

4. 名单配置与现场应急:改数组、换字体、防误触

4.1 名单数组的三种配置方式

名单来源通常有三种:直接写在 JS 里、从外部 JSON 加载、从 CSV 粘贴。现场最稳的是第一种,因为不依赖网络和文件读取。但如果名单有几百人,手写数组容易出错,我一般会写一个转换函数,把「每行一个名字」的文本转成数组。

// 方式一:直接写数组 const allNames = ["张三", "李四", "王五"]; // 方式二:从 textarea 粘贴转换 function parseNamesFromText(text) { return text .split("\n") .map(name => name.trim()) .filter(name => name.length > 0); } // 使用示例 const rawText = `张三 李四 王五`; const namesFromText = parseNamesFromText(rawText);

split("\n")按换行切分,map去掉首尾空格,filter过滤空行。这个函数可以挂在一个隐藏的textarea上,现场如果临时加人,直接粘贴进去点「更新名单」就行。注意:Windows 的换行是\r\n,split("\n")后每行末尾会带\r,trim()能去掉,所以不用额外处理。

4.2 现场防误触与快捷键设计

年会现场最怕什么?主持人手一抖,还没准备好就抽了。所以按钮要有防连点,最好再加个快捷键。我一般用disabled属性控制按钮状态,滚动时禁用「开始」,停止后禁用「停止」。

const btnStart = document.getElementById("btnStart"); btnStart.addEventListener("click", () => { if (isRolling) { stopAndReveal(); btnStart.textContent = "开始抽奖"; } else { startRoll(); btnStart.textContent = "停止"; } }); // 空格键快捷操作 document.addEventListener("keydown", (e) => { if (e.code === "Space") { e.preventDefault(); btnStart.click(); } });

按钮文字在「开始抽奖」和「停止」之间切换,比两个按钮更省空间。空格键监听用e.code === "Space",比e.key === " "更稳,不受输入法影响。e.preventDefault()防止页面滚动。注意:如果页面上有输入框,空格键会冲突,所以这个快捷键只在没有焦点输入框时生效,实际项目中可以加个判断。

4.3 字体加载失败与降级方案

大屏上字体很重要,但现场电脑不一定有你用的字体。如果用了自定义字体,加载失败会变成默认宋体,瞬间掉档次。我一般用font-family降级链,并且优先用系统自带的无衬线字体。

.name-display { font-family: "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif; font-weight: bold; }

Microsoft YaHei在 Windows 上基本都有,PingFang SC是 macOS 的,后面跟Arial和sans-serif兜底。如果现场是 Linux 系统,可能都没有,那就只能靠sans-serif。所以如果条件允许,提前把字体文件转成woff2放在同目录,用@font-face引入,但要注意文件路径不能错,否则一样加载失败。

5. 避坑与常见问题:现场翻车记录

5.1 名字滚动太快导致看不清

现象:滚动时名字一闪而过,台下根本看不清在滚什么。原因:requestAnimationFrame每帧都换名字,60Hz 下每秒换 60 次,太快了。解决:加一个时间戳判断,每 80 毫秒才换一次名字。

let lastChange = 0; function roll(timestamp) { if (timestamp - lastChange > 80) { const randomIndex = Math.floor(Math.random() * names.length); display.textContent = names[randomIndex]; lastChange = timestamp; } rollTimer = requestAnimationFrame(roll); }

5.2 抽奖结果重复出现同一个人

现象:同一个人连续中两次奖。原因:remainingNames没有正确splice,或者每次抽奖都从allNames里取。解决:确认drawOne里用的是remainingNames,并且splice后没有再重新赋值。如果允许重复中奖,那另说,但默认去重。

5.3 现场投影分辨率导致布局错位

现象:笔记本上好好的,投到 1920×1080 大屏上名字偏左或者按钮跑到屏幕外。原因:用了固定px或者margin负值。解决:所有尺寸用vw/vh/vmin,布局用flex居中,不要用绝对定位写死坐标。

5.4 按钮点击没反应

现象:点了「开始抽奖」没动静。原因:isRolling状态没重置,或者rollTimer没取消导致逻辑卡死。解决:在startRoll开头加if (isRolling) return;,在stopRoll里确保cancelAnimationFrame传的是正确的 id。另外检查控制台有没有报错,最常见的是getElementById拿不到元素,因为script放在head里了。

5.5 名单里有空行导致抽到空白

现象:抽奖时偶尔显示空白。原因:名单数组里有空字符串。解决:在parseNamesFromText里用filter过滤,或者在drawOne里加判断,如果抽到空字符串就重新抽。但最好在数据源头就过滤掉。

6. 进阶技巧:把抽奖结果导出成表格并自动备份

现场抽完奖,行政通常要一份中奖名单。如果只靠手抄,容易漏。我一般会在页面里加一个「导出」按钮,把winners数组转成 CSV 下载。这个功能平时用不上,但抽完奖那一刻,行政看你的眼神都不一样。

function exportWinners() { if (winners.length === 0) { alert("还没有中奖记录"); return; } const header = "姓名\n"; const content = header + winners.join("\n"); const blob = new Blob([content], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `年会中奖名单_${new Date().toLocaleDateString()}.csv`; link.click(); URL.revokeObjectURL(url); }

Blob的type里加了charset=utf-8,否则中文在 Excel 里打开会乱码。URL.createObjectURL生成临时链接,link.click()触发下载,最后revokeObjectURL释放内存。文件名带了日期,避免多次导出覆盖。这个函数可以绑在控制层的一个小按钮上,平时不显眼,需要时一点就有。

还有一个进阶玩法:用localStorage自动备份中奖记录,防止页面刷新丢失。每次drawOne之后把winners存进去,页面加载时读出来。

function saveWinners() { localStorage.setItem("luckyWinners", JSON.stringify(winners)); } function loadWinners() { const saved = localStorage.getItem("luckyWinners"); if (saved) { winners.push(...JSON.parse(saved)); } }

JSON.stringify把数组转成字符串,JSON.parse还原。注意:localStorage只能存字符串,不能直接存数组。另外,如果现场用的是公共电脑,记得抽完奖清一下,否则下一场活动会读到上一场的数据。

从那以后我每次做抽奖页面,都会在drawOne里加一行saveWinners(),并且把导出按钮放在最显眼的位置。年会现场不怕一万就怕万一,多一个备份就少一次手忙脚乱。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/1 1:36:56

C++模板进阶:特化、SFINAE与CRTP实战指南

C模板进阶及特化实战指南写了好多年C&#xff0c;我越来越觉得模板能不能玩明白&#xff0c;基本决定了你对这门语言的理解深度。很多朋友一开始都会用vector<int>、写个简单的函数模板&#xff0c;感觉模板好像也就是个“通用类型工具”。但等你真的想在一个类里对不同类…

作者头像 李华
网站建设 2026/10/30 23:45:17

游戏本跑1250亿参数大模型:Strata推理引擎实战与优化指南

1. 一台游戏本跑 1250 亿参数模型&#xff0c;这事到底靠不靠谱先说结论&#xff1a;靠谱&#xff0c;但有前提。Strata 这个项目最近在圈子里讨论度很高&#xff0c;核心卖点就一句话——让普通游戏电脑跑起 1250 亿参数的大模型。注意这里的措辞是"跑"&#xff0c;…

作者头像 李华
网站建设 2026/10/28 9:08:03

医院HIS系统检验报告公式Web化迁移实践:从OCR到KaTeX渲染

医院HIS系统里的检验报告要上Web端&#xff0c;最让人头疼的往往不是数据接口打通&#xff0c;也不是权限模型设计&#xff0c;而是那些混在报告单里的公式。我去年接了一个老HIS系统改造的活儿&#xff0c;检验报告模块要从C/S架构迁到B/S架构&#xff0c;业务方提的需求里有一…

作者头像 李华
网站建设 2026/10/23 6:44:51

RAG法律问答系统实战:从混合检索到参数调优

简介&#xff1a;基于RAG架构的智能法律问答系统项目&#xff0c;定位于法律咨询场景下的智能问答实现&#xff0c;面向自然语言处理、法律信息化方向的开发者、科研人员及高校学生。项目整合检索与生成两条技术路径&#xff0c;先在大规模法律文档库中检索相关片段&#xff0c…

作者头像 李华
网站建设 2026/10/23 5:01:16

隔离内网部署AI Agent实战:MCP协议与Skills封装指南

1. 为什么要在隔离内网里折腾 AI Agent第一次接到"隔离内网部署 AI Agent"这个需求时&#xff0c;我的第一反应是&#xff1a;这不是自找麻烦吗&#xff1f;外网环境下一行pip install就能搞定的事&#xff0c;放到隔离内网里&#xff0c;光是依赖包搬运就能耗掉一整…

作者头像 李华
网站建设 2026/10/23 4:14:00

士兵扫雷H5源码解析:从扫雷算法到签到系统与部署避坑

简介&#xff1a;H5士兵扫雷6.0修复版完整源码包&#xff0c;附带部署与开发教程&#xff0c;面向需要快速搭建或二次开发扫雷类小游戏的开发者与运营人员。整合了签到系统与Z支付接口&#xff0c;并对卡顿、闪退等稳定性问题做了针对性修复&#xff0c;在低端设备上也能保持流…

作者头像 李华