当前位置: 首页 > news >正文

本地运行的年会抽奖工具,改JS名单就能抽,中奖实时可见

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

简介:直接双击index.html就能用的年会抽奖页面,完全跑在浏览器里,不用装服务器、不连后台、不传数据。所有参与人名字写在member.js里,打开文件删增改名就能更新名单,保存后刷新网页立即生效。点‘开始抽奖’按钮,姓名快速滚动,再点‘停止’就定格中奖者,名字自动高亮并追加到下方获奖区,支持一轮轮连续抽,历史结果一直保留在页面上不消失。配好了节日感背景图(bg.png、bg2.png)、奖状样式(borad.png)、提示图标(alert.png、alert2.png)和手写风字体(xk.ttf),logo.ico和logo.png可替换成公司标识。tagcanvas.js负责让标题文字带动态旋转效果,增强现场氛围。README.md写了三步操作说明:改名单、换Logo、本地打开,行政、HR或活动执行人员拿到就能上手,适合内网、投影仪或笔记本单机使用。

1. 项目概述:为什么一个“纯前端年会抽奖工具”能成为行政人员的救命稻草?

你有没有经历过这种场面:年会倒计时3小时,IT同事在会议室调试投影仪,HR刚收到最后一版名单Excel,行政小妹抱着笔记本冲进现场,手忙脚乱打开一个叫“抽奖.exe”的程序——结果双击没反应,提示“缺少MSVCP140.dll”,再换一个,又弹出“此应用无法在你的电脑上运行”。台下领导已经开始看表,供应商在后台催流程单,而你连中奖音效都没调出来。

这个本地运行的年会抽奖工具,就是为这种真实场景而生的。它不依赖任何服务器、不安装任何后台服务、不上传一丁点数据到云端,所有逻辑都在浏览器里跑完。你双击index.html,页面立刻加载;改完member.js里的名字列表,按 Ctrl+S 保存,刷新网页,新名单就生效了——整个过程像编辑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.js
    javascript // 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 旋转依赖perspectivetransform-style: preserve-3d,但在不同显卡驱动下表现差异极大:NVIDIA 显卡可能流畅,而Intel HD Graphics 4000(很多老款商务本标配)会出现文字撕裂、闪烁甚至白屏;Three.js 更是重量级,压缩后仍超300KB,加载慢不说,初始化失败时控制台报错晦涩,行政人员根本无法排查。

tagcanvas.js是一个轻量级(仅48KB)、专为文字云设计的Canvas库,它不依赖WebGL,纯用2D Canvas绘制,兼容性覆盖IE9+、所有现代浏览器。更重要的是,它提供了极简API:

TagCanvas.Start('myCanvas', 'myText', { textColour: '#FFD700', outlineColour: '#FF8C00', maxSpeed: 0.05, depth: 0.8 });

只要页面有个<canvas id="myCanvas"><div id="myText">幸运大转盘</div>,三行代码就搞定。我们甚至做了降级处理:如果Canvas初始化失败(比如浏览器禁用Canvas),它会自动回退到纯CSS文字阴影+轻微抖动,保证标题始终可见且有节日感。这种“优雅降级”思维,正是面向非技术用户的终极体贴。

3. 核心细节解析与实操要点:从改名单到换Logo的全流程拆解

3.1 修改抽奖名单:三步完成,零风险操作指南

行政人员最常做的操作就是更新名单。以下是详细步骤和避坑点:

第一步:找到并打开 member.js 文件
- 资源包解压后,进入js文件夹,双击member.js
- 推荐用系统自带记事本(Windows)或TextEdit(Mac),不要用Word或WPS——它们会插入不可见的格式字符(如全角空格、软回车),导致JS语法错误。如果习惯用VS Code,务必关闭“自动格式化”和“保存时清理空白行”选项(设置里搜files.trimTrailingWhitespace设为false)。

第二步:编辑数组内容
- 找到const MEMBERS = [开头的行,数组元素每行一个,用英文逗号分隔。例如:
javascript const MEMBERS = [ "张三(市场部)", "李四(研发一部)", "王五(财务部)", // 以下为实习生,仅参与三等奖 "赵六(实习)", "钱七(实习)" ];
-关键技巧:名字里可以加括号备注部门,不影响抽奖逻辑(程序只取完整字符串显示),但能避免现场念错人;注释行以//开头,会被JS引擎忽略,方便标注规则。

第三步:保存并刷新网页
- 按 Ctrl+S(Cmd+S)保存文件,务必确认保存对话框里文件类型是“所有文件”而非“文本文档”(记事本常见陷阱,否则会变成member.js.txt);
- 切换到已打开的index.html页面,按 F5 刷新;
- 页面顶部会显示绿色提示:“✅ 名单已更新,共XX人参与抽奖”,同时右下角弹出alert2.png图标提示。

注意:如果刷新后名单没变,90%是文件没保存成功或保存成了.txt后缀。此时打开浏览器开发者工具(F12),切换到 Console 标签页,输入MEMBERS回车——如果显示undefined,说明member.js没加载;如果显示旧数组,说明你编辑的是另一个同名文件(比如桌面也存了一份备份)。

3.2 替换品牌标识:ico 和 png 的双重适配逻辑

资源包提供logo.icologo.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 src="logo.png"的路径是否正确。有些解压软件会把文件夹层级搞乱(比如把logo.png解压到根目录,而HTML里写的是./logo.png),此时需调整路径或统一放在根目录。

3.3 节日背景图的切换机制与性能优化

资源包含bg.pngbg2.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: cover+background-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 class="prize-no">#${prizeHistory.length + 1}</span> <span class="prize-name">${name}</span> <span class="prize-time">${new Date().toLocaleTimeString()}</span> <span class="prize-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;charset=utf-8,URL方案),无需后端。

4.3 音效与视觉反馈的协同设计:让每一次中奖都“可感知”

中奖瞬间的体验由三重反馈叠加构成,缺一不可:

  • 听觉反馈:播放win-sound.wav(base64编码嵌入JS)
    javascript 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.js,Ctrl+S 保存,F5刷新index.html,名单没变。她反复操作三次,越来越慌。

排查路径
1. 打开开发者工具(F12)→ Network 标签页 → 刷新页面;
2. 在资源列表中找到member.js,看它的 Status 列——如果是200(从服务器加载),说明正常;如果是(from disk cache)(from memory cache),说明浏览器用了缓存;
3. 点击该行,在 Headers 标签页查看Response Headers中的Cache-Control字段,如果是max-age=3600,证明缓存1小时。

解决方案(三选一)
-快捷法:按Ctrl+F5强制刷新(忽略缓存);
-根治法:在index.html<script>标签里加时间戳参数:
```html

`` 每次改名单后,把v=后面的日期改成当天,浏览器就会当作新资源加载; - **一劳永逸法**:在member.js文件末尾加一行注释// Updated: 2024-12-15 14:30`,每次保存都改时间,利用注释变化触发缓存失效。

实操心得:我在给5家客户部署时,都会在README.md里用加粗字体写:“⚠️ 修改名单后,请务必按 Ctrl+F5 强制刷新!普通F5可能不生效。”

5.2 “投影仪上名字显示不全,右边被切掉了!”——响应式断点与字体渲染玄学

某次制造业年会,现场用松下PT-VW340投影仪(1024×768分辨率),页面右侧名字被截断,技术同事检查CSS发现font-size: 2.5rem在1024宽度下溢出。

根本原因rem是相对于根元素字体大小的单位,而根元素<html>font-size设为62.5%(即10px),2.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底部写了<script>document.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打开Console
2. 查看是否有Failed to load resource错误
3. 点击错误链接,确认文件是否存在
检查src属性路径(如./js/member.js是否应为js/member.js);用记事本重新保存member.js
中奖名字高亮但没追加到获奖区prize-history元素ID拼写错误,或CSS设置了display:none1. F12选中获奖区,看元素ID是否为prize-history
2. 在Elements面板搜索prize-history
确认HTML中<div id="prize-history">拼写;检查CSS是否有#prize-history { display: none; }
点击停止后名字还在滚动浏览器禁用了requestAnimationFrame(极罕见)或stopPrize函数被覆盖1. Console输入typeof stopPrize
2. 若返回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.ttf),logo.ico和logo.png可替换成公司标识。tagcanvas.js负责让标题文字带动态旋转效果,增强现场氛围。README.md写了三步操作说明:改名单、换Logo、本地打开,行政、HR或活动执行人员拿到就能上手,适合内网、投影仪或笔记本单机使用。


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

http://www.jsqmd.com/news/988750/

相关文章:

  • 2026年惠州中央空调回收品牌推荐与选择攻略 - 广东再生资源回收
  • 揭秘AI教材写作技巧:低查重工具加持,5天完成30万字教材编写!
  • 2026 天津漏水检测与修缮机构实测盘点 5 家合规服务商参考(含卫生间专项维修) - 宅安选房屋修缮
  • 数字电源开发实战:JTAG与SCI接口在DSC调试中的协同应用
  • Beyond Compare 5终极激活指南:5分钟解锁完整功能
  • 计算机毕业设计基于ECharts的电脑销售信息可视化平台设计与实现
  • Flutter+Go微服务架构:点餐源码系统小程序性能优化实战(附代码)
  • 欧奥电子车载移动UFS4.1验证:mSMP与B2B 高保真探测技术详解
  • i.MX 7ULP异构多核架构解析:平衡性能与功耗的嵌入式设计实践
  • 2026年6月成都本地人私藏高分火锅合集|全店口碑4.8分+,闭眼吃不踩坑 - TOP10品牌推荐榜单
  • 恒流IC/ NU402在LED模组中的应用
  • 描述符(Descriptors)‌
  • 2026年绕线机厂家推荐榜:电线电缆/铜丝钢丝/高速伺服自动绕线机优质品牌深度解析 - 品牌发掘
  • 第 15 集:Claude Code上下文工程学 —— 根治“80% 问题”
  • 2026年6月佛山回收中央空调公司推荐,正规资质环保处理更合规 - 广东再生资源回收
  • 当信号与系统遇见深度学习:我用傅里叶变换和拉普拉斯算子,看懂了CNN的本质
  • 如何打造个人专属的数字记忆库:从微信数据到生活足迹的完整指南
  • Luminex多因子免疫检测技术革新,云克隆七因子体系实现Th1/Th2/Th17免疫平衡全景量化
  • 实现图片本地缓存,减少url重复请求
  • AZ系、ZK系、WE系——一张牌号选型对照,加四种成型工艺的匹配逻辑
  • 有哪些真正好用的降AIGC网站?能同时搞定知网查重和降低AIGC率的那种
  • 非技术背景AIPM技术学习攻略:不学废、不内卷、刚好够用
  • AIOps 智能日志模式挖掘与异常关联:从日志海洋到结构化洞察
  • 数据的加密与解密(23:32)
  • 微信聊天记录永久备份终极指南:用WeChatExporter完整保存你的数字记忆
  • 集合 USB,AI ENC,AEC,BF,全面功能的语音处理模组
  • 2026年腾讯云OpenClaw/Hermes Agent配置Token Plan保姆级教程分享
  • 光伏电缆厂家盘点:从资质产能看选型适配方向 - 互联网科技品牌测评
  • 深入探讨KDB+函数的秩和参数验证
  • RedPanda-CPP轻量级C/C++ IDE架构解析与性能优化对比