基于tagcanvas.js的可审计年会抽奖前端系统
2026/9/16 16:38:07 网站建设 项目流程

简介:这是一套面向企业IT人员与PHP初学者的年会线上抽奖网站源码,专为公司内部年会活动快速搭建轻量级抽奖平台而设计,解决传统现场抽奖组织难、透明度低、参与感弱等问题。资源共26个文件,含5个JavaScript脚本(实现抽奖逻辑、动态渲染与交互)、1个HTML主页面、1个CSS样式文件、16张PNG/JPG图片(含背景、奖品图标、提示弹窗等UI素材)、1个字体文件(xk.ttf)及1个ICO图标,整体压缩包仅5.48MB,部署便捷,无需复杂环境配置。已有637人学习下载,适合希望快速落地内部活动工具、理解PHP+HTML前后端协同逻辑的开发者。读者可直接部署运行,完整掌握用户登录、奖项配置、随机公平抽奖、中奖结果实时展示等核心模块实现方式,并通过源码学习前端Canvas动画(tagcanvas.js)、响应式布局与基础安全防护实践。

1. 年会抽奖活动程序源码不是“点开即用”的网页,而是可定制、可验证、可审计的前端交互系统

很多公司行政或IT同事在年会前一周紧急搜索“年会抽奖活动程序源码”,下载 ZIP 包双击index.html后发现:名单导不进、奖品顺序乱、中奖音效不响、多人同时点击会重复中奖——问题不在代码“坏”,而在于它本质是一套需本地部署、参数可配置、行为可追溯的前端应用系统,不是静态演示页。这套源码的核心价值,是把「随机性可控」「过程可回溯」「界面可适配」「数据不离场」四件事封装进 HTML+JS+CSS 技术栈,尤其依赖tagcanvas.js实现可视化球体滚动效果。它适合行政人员配合前端基础能力者(懂改 JSON、调 CSS、查浏览器控制台)快速落地,也适合有 JS 调试经验的开发者做二次开发——比如对接企业微信通讯录 API、增加中奖人脸打码功能、或导出带时间戳的中奖记录 CSV。如果你需要的是“上传 Excel 就能抽”的 SaaS 工具,这源码不是终点;但如果你要的是一次部署、多年复用、全程自主、无外部依赖的年会抽奖底座,它就是最轻量可靠的起点。

2. 用 tagcanvas.js 搭建可旋转球体抽奖池:从初始化到动态渲染的完整链路

tagcanvas.js是本源码实现视觉核心的关键库,它并非通用 3D 引擎,而是专为标签云(tag cloud)设计的 Canvas 2D 粒子动画库。在年会场景中,它被巧妙复用为“滚动球体”容器:每个员工姓名作为独立标签(tag),按球面坐标分布,通过TagCanvas.Start()启动旋转动画,再结合TagCanvas.SetWeight()控制权重(如部门人数加权),最终在停止时取视觉中心点坐标对应的标签作为中奖者。这种方案比纯 CSS 3D 或 Three.js 更轻量(压缩后仅 42KB)、兼容性更好(IE9+ 支持),且动画帧率稳定——这对年会大屏实时展示至关重要。

2.1 初始化 tagcanvas 容器与基础参数配置

首先确保 HTML 中存在唯一 ID 的<canvas>元素,并引入tagcanvas.js(注意:必须使用 v2.9+ 版本,v2.8 存在mouseMove事件在 Chrome 115+ 下失效的 bug):

<canvas id="myCanvas" width="800" height="600"></canvas> <script src="js/tagcanvas.min.js"></script>

初始化代码需在 DOM 加载完成后执行,关键参数如下(写入js/main.js):

// main.js document.addEventListener('DOMContentLoaded', function () { try { TagCanvas.Start('myCanvas', 'myTags', { textColour: '#333', outlineColour: '#fff', maxSpeed: 0.05, // 球体自转最大角速度,值越小越稳,建议 0.03~0.07 depth: 0.8, // 球体 Z 轴深度系数,影响透视感,0.6~0.9 可调 reverse: true, // 使球体逆时针旋转,符合中文阅读习惯 noMouse: false, // 必须为 false,否则无法响应鼠标悬停暂停 weight: true, // 启用权重计算,用于后续按部门加权 weightMode: 'size', // 权重映射为字体大小,直观体现概率差异 initial: [0, -0.2] // 初始旋转角度 [x, y],-0.2 让球体略微俯视,突出中心区域 }); } catch (e) { console.error('TagCanvas 初始化失败:', e); alert('请检查 canvas ID 是否正确,或浏览器是否禁用了 JavaScript'); } });

提示maxSpeed是最常调整的参数。年会现场灯光强、大屏远,过快的旋转会导致人眼无法聚焦中心点。实测 0.04 是平衡流畅性与可读性的临界值;若现场有儿童或老年员工,建议降至 0.025 并延长启动时间。

2.2 动态注入员工数据并绑定权重逻辑

员工名单不硬编码在 JS 中,而是通过 JSON 文件加载(如data/employees.json),结构需严格遵循tagcanvas要求的text+weight字段:

// data/employees.json [ {"text": "张三(技术部)", "weight": 1.2}, {"text": "李四(市场部)", "weight": 0.8}, {"text": "王五(财务部)", "weight": 1.0} ]

权重weight值决定该员工在球体表面的“停留密度”——weight越高,相同区域内生成的标签实例越多,被选中的概率线性提升。加载逻辑如下:

// main.js 续写 function loadEmployees() { return fetch('data/employees.json') .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); return res.json(); }) .then(data => { // 验证数据格式 if (!Array.isArray(data) || data.length === 0) { throw new Error('员工数据格式错误:必须是非空数组'); } const validItems = data.filter(item => typeof item.text === 'string' && typeof item.weight === 'number' && item.weight > 0 ); if (validItems.length !== data.length) { console.warn('部分员工数据权重无效,已过滤'); } return validItems; }); } // 在 DOMContentLoaded 内调用 loadEmployees().then(employees => { // 创建隐藏的 <div id="myTags"> 容器,供 tagcanvas 读取 const tagsDiv = document.createElement('div'); tagsDiv.id = 'myTags'; tagsDiv.style.display = 'none'; employees.forEach(emp => { const span = document.createElement('span'); span.textContent = emp.text; span.setAttribute('data-weight', emp.weight.toString()); // 保留原始权重值 tagsDiv.appendChild(span); }); document.body.appendChild(tagsDiv); // 此时再调用 TagCanvas.Start,确保 myTags 已存在 TagCanvas.Start('myCanvas', 'myTags', { /* 参数同上 */ }); }).catch(err => { console.error('员工数据加载失败:', err); document.getElementById('status').textContent = '名单加载失败,请检查 data/employees.json'; });

注意tagcanvasweight机制是渲染层加权,而非算法层随机数加权。这意味着它通过增加高权重员工在球体表面的标签密度来提升视觉命中率,实际中奖仍由TagCanvas.GetNearestTag()在停止瞬间按像素坐标判定。这种方式规避了伪随机数种子被预测的风险,符合年会“公开透明”的心理预期。

3. 实现抽奖核心逻辑:启动、暂停、结果锁定与防重抽的 JS 控制流

抽奖按钮不是简单触发Math.random(),而是一套状态机驱动的 Canvas 动画控制流程。整个过程分为「启动加速 → 匀速旋转 → 手动暂停 → 坐标锁定 → 结果渲染」五阶段,每阶段需精确控制tagcanvas的内部状态和 UI 反馈。关键难点在于:如何在暂停瞬间准确获取视觉中心点的标签?以及如何防止网络延迟或误触导致重复中奖?

3.1 构建可中断的旋转状态机与暂停时机判定

tagcanvas本身不提供“暂停”API,需通过覆盖其内部animFrame循环实现。我们监听鼠标点击(或触摸)事件,在requestAnimationFrame回调中插入中断标记:

// main.js 续写:定义全局状态 let isSpinning = false; let spinStartTime = 0; let spinDuration = 0; // 当前已旋转毫秒数 const MIN_SPIN_DURATION = 5000; // 强制最少旋转 5 秒,避免过早暂停 // 启动抽奖函数 function startLottery() { if (isSpinning) return; isSpinning = true; spinStartTime = Date.now(); spinDuration = 0; // 更新 UI 状态 document.getElementById('startBtn').disabled = true; document.getElementById('status').textContent = '抽奖进行中...'; // 启动动画循环 function animate() { if (!isSpinning) return; spinDuration = Date.now() - spinStartTime; // 检查是否达到最小旋转时间 if (spinDuration >= MIN_SPIN_DURATION) { // 监听下一次鼠标点击,作为暂停信号 const stopHandler = () => { isSpinning = false; document.removeEventListener('click', stopHandler); document.removeEventListener('touchend', stopHandler); lockWinner(); }; document.addEventListener('click', stopHandler); document.addEventListener('touchend', stopHandler); } // 继续请求下一帧 requestAnimationFrame(animate); } requestAnimationFrame(animate); } // 绑定按钮事件 document.getElementById('startBtn').addEventListener('click', startLottery);

提示MIN_SPIN_DURATION不是技术限制,而是仪式感设计。年会现场需要足够长的悬念时间,5 秒是经多次测试的黄金值——短于 4 秒观众来不及反应,长于 6 秒易引发焦躁。此参数应写入配置文件(如config.json),方便行政人员无需改代码即可调整。

3.2 坐标锁定与防重抽:基于 Canvas 像素坐标的精准命中

暂停后,tagcanvas仍在内存中维持所有标签的 3D 坐标。我们通过TagCanvas.GetNearestTag(x, y)方法,传入 Canvas 中心点像素坐标,获取距离最近的标签元素:

function lockWinner() { const canvas = document.getElementById('myCanvas'); const rect = canvas.getBoundingClientRect(); const centerX = rect.left + canvas.width / 2; const centerY = rect.top + canvas.height / 2; // 获取 Canvas 坐标系下的中心点(需减去页面滚动偏移) const pageX = centerX - window.scrollX; const pageY = centerY - window.scrollY; // 转换为 Canvas 本地坐标(考虑 devicePixelRatio) const dpr = window.devicePixelRatio || 1; const localX = (pageX - rect.left) * dpr; const localY = (pageY - rect.top) * dpr; try { const winnerTag = TagCanvas.GetNearestTag(localX, localY); if (!winnerTag || !winnerTag.element) { throw new Error('未检测到有效中奖标签'); } const winnerText = winnerTag.element.textContent.trim(); const winnerWeight = parseFloat(winnerTag.element.getAttribute('data-weight') || '1'); // 防重抽:从 DOM 中移除该标签,并更新数据源 winnerTag.element.remove(); // 渲染结果到页面 renderWinner(winnerText, winnerWeight); } catch (err) { console.error('中奖判定失败:', err); document.getElementById('status').textContent = '中奖识别失败,请重试'; } } function renderWinner(name, weight) { const resultEl = document.getElementById('winnerResult'); resultEl.innerHTML = ` <h2>🎉 恭喜中奖!</h2> <p class="winner-name">${name}</p> <p class="winner-weight">权重系数:${weight.toFixed(1)}</p> `; resultEl.style.display = 'block'; // 播放音效(需提前预加载 audio 元素) const audio = document.getElementById('winSound'); if (audio) audio.currentTime = 0; if (audio && audio.play) audio.play().catch(e => console.log('音效播放被阻止')); // 重置按钮状态 document.getElementById('startBtn').disabled = false; document.getElementById('status').textContent = '等待下一轮抽奖'; }

注意GetNearestTag()返回的是tagcanvas内部对象,其element属性指向原始 DOM<span>。直接调用element.remove()即可从球体中永久移除该员工,实现“抽过即删”。这是防重抽最可靠的方式——不依赖后端数据库或 localStorage 标记,完全在前端内存中完成状态同步。

4. CSS 交互增强与响应式适配:从鼠标悬停反馈到多屏尺寸兼容

年会现场设备多样:LED 大屏(4K 分辨率)、笔记本投屏(1366×768)、甚至手机临时查看(375×667)。CSS 不仅负责美化,更要承担「操作引导」和「状态反馈」功能。本源码采用移动优先策略,利用@media查询 +rem单位 +:hover/:active伪类,实现跨设备一致体验。特别地,css 鼠标移入事件被用于暂停动画、高亮候选区,这是提升现场互动感的关键细节。

4.1 构建可悬停暂停的球体交互层

默认情况下tagcanvas的 Canvas 元素无法直接响应:hover,需在其上方叠加一层透明<div>作为事件代理层,并通过pointer-events: none确保底层 Canvas 仍可接收click

/* css/style.css */ #canvas-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; /* 透传鼠标事件到底层 Canvas */ z-index: 10; } #canvas-overlay:hover { pointer-events: auto; /* 悬停时自身捕获事件 */ cursor: pointer; } #canvas-overlay:hover ~ #myCanvas { opacity: 0.95; /* 悬停时 Canvas 微降透明度,暗示可操作 */ } /* 按钮悬停反馈 */ #startBtn:hover { transform: scale(1.05); box-shadow: 0 0 15px rgba(255, 204, 0, 0.6); } #startBtn:active { transform: scale(0.98); }

HTML 结构需对应调整:

<div id="canvas-container" style="position: relative;"> <canvas id="myCanvas" width="800" height="600"></canvas> <div id="canvas-overlay"></div> <!-- 事件代理层 --> </div>

提示pointer-events: none是实现“视觉层与事件层分离”的核心。它让悬浮提示、高亮边框等装饰效果不干扰底层 Canvas 的动画渲染,同时又能通过:hover触发 JS 逻辑(如显示“点击暂停”提示文字)。这种模式比监听mousemove事件更高效,且无性能损耗。

4.2 响应式断点与字体/间距的 rem 动态缩放

为适配不同屏幕,定义 4 个断点,并将所有尺寸单位转为rem(以html根字体为基准):

/* css/style.css */ html { font-size: 16px; /* 默认 1rem = 16px */ } @media screen and (max-width: 768px) { html { font-size: 14px; } /* 平板:1rem = 14px */ } @media screen and (max-width: 480px) { html { font-size: 12px; } /* 手机:1rem = 12px */ } @media screen and (min-width: 1920px) { html { font-size: 18px; } /* 4K 大屏:1rem = 18px */ } /* 使用 rem 的示例 */ #winnerResult { padding: 2rem 1.5rem; font-size: 1.8rem; line-height: 1.4; } .winner-name { font-size: 3.2rem; margin: 1rem 0; color: #e67e22; text-shadow: 0 0 20px rgba(230, 126, 34, 0.5); } /* 三行模式的 CSS 文件结构:基础重置 → 布局 → 组件 → 响应式 */ /* 此处省略重置和布局代码,聚焦组件与响应式 */

注意rem缩放必须配合viewportmeta 标签,且禁止在移动端使用user-scalable=no(会阻碍视力障碍者放大页面)。本源码默认启用缩放,符合 WCAG 2.1 AA 标准。

5. 数据安全与现场应急技巧:JSON 名单校验、离线缓存及一键重置方案

年会现场最怕三件事:名单文件损坏、网络突然中断、主持人误操作连抽两人。本源码通过「客户端 JSON Schema 校验」「Service Worker 离线缓存」「内存快照式重置」三重机制应对。所有操作均不依赖后端,100% 前端自治。

5.1 员工 JSON 的结构化校验与错误定位

employees.json加载后,不仅检查数组格式,还逐条验证字段语义。当发现无效数据时,返回具体行号和错误类型,而非笼统报错:

// main.js 续写:增强版校验函数 function validateEmployeeData(data) { const errors = []; data.forEach((item, index) => { if (typeof item.text !== 'string' || item.text.trim().length === 0) { errors.push(`第 ${index + 1} 行:text 字段缺失或为空`); } if (typeof item.weight !== 'number' || item.weight <= 0) { errors.push(`第 ${index + 1} 行:weight 必须为大于 0 的数字,当前值:${item.weight}`); } }); if (errors.length > 0) { throw new Error(`员工数据校验失败:\n${errors.join('\n')}`); } return true; } // 在 loadEmployees().then() 中调用 loadEmployees().then(data => { validateEmployeeData(data); // 插入此处 // ...后续渲染逻辑 });

提示:错误信息包含行号(index + 1),行政人员可直接打开 JSON 文件跳转到对应行修改,无需理解 JSON Schema 语法。这是降低使用门槛的关键细节。

5.2 Service Worker 离线缓存:让抽奖网站在断网时依然可用

生成sw.js文件,缓存所有静态资源(HTML/CSS/JS/JSON/Audio):

// sw.js const CACHE_NAME = 'lottery-v1.2'; const urlsToCache = [ '/', '/index.html', '/css/style.css', '/js/tagcanvas.min.js', '/js/main.js', '/data/employees.json', '/audio/win.mp3' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) .then(() => self.skipWaiting()) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) ); });

index.html中注册:

<script> if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('SW registered:', reg.scope)) .catch(err => console.log('SW registration failed:', err)); }); } </script>

注意sw.js必须与index.html同域,且通过 HTTPS 或localhost提供。年会现场用http-server -c-1启动本地服务即可满足条件,无需配置 Nginx。

5.3 一键重置:恢复初始状态的内存快照方案

为应对误操作,添加「重置名单」按钮,不重新加载 JSON,而是恢复内存中原始数据快照:

// main.js 续写:在 loadEmployees().then() 中保存快照 let originalEmployees = []; loadEmployees().then(data => { originalEmployees = [...data]; // 深拷贝原始数据 // ...渲染逻辑 }); // 重置函数 function resetLottery() { // 清空当前所有标签 const tagsDiv = document.getElementById('myTags'); if (tagsDiv) tagsDiv.innerHTML = ''; // 重新注入原始数据 originalEmployees.forEach(emp => { const span = document.createElement('span'); span.textContent = emp.text; span.setAttribute('data-weight', emp.weight.toString()); tagsDiv.appendChild(span); }); // 重启 tagcanvas TagCanvas.Reload('myCanvas', 'myTags'); // 重置 UI document.getElementById('winnerResult').style.display = 'none'; document.getElementById('status').textContent = '已重置,可重新开始抽奖'; } document.getElementById('resetBtn').addEventListener('click', resetLottery);

提示TagCanvas.Reload()是官方提供的热重载 API,比销毁重建更快,且保留原有配置参数。此方案确保即使抽到最后一人,也能一键回到全员待抽状态,无需重启浏览器或重新选择文件。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询