ARTICLE DETAIL

资讯详情

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

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

纯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 langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title年会抽奖/title link relstylesheet hrefstyle.css /head body !-- 背景层粒子/光效纯装饰 -- div classbg-layer/div !-- 滚动层显示当前滚动的名字 -- div classroll-layer div classname-display idnameDisplay准备开始/div /div !-- 控制层奖项切换 开始/停止 -- div classcontrol-layer div classprize-tabs idprizeTabs/div button classbtn-start idbtnStart开始抽奖/button /div script srcmain.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 到 requestAnimationFrame3.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\nsplit(\n)后每行末尾会带\rtrim()能去掉所以不用额外处理。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;charsetutf-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里加了charsetutf-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()并且把导出按钮放在最显眼的位置。年会现场不怕一万就怕万一多一个备份就少一次手忙脚乱。希望帮到你。本文还有配套的精品资源点击获取
返回列表