
简介一款基于HTML5实现的手机验证抽奖领券效果源码包面向Web前端学习者、活动运营人员以及需要在移动端快速搭建互动营销页面的开发者解决了从手机号校验到抽奖结果反馈这一完整交互流程的实现问题。源码使用HTML5结构化标记构架页面配合CSS增强视觉与适配效果同时引入JavaScript处理手机号验证、抽奖算法、中奖提示等核心逻辑还可借助Canvas或SVG绘制动态转盘或领券动画提升用户体验。压缩包共29个文件其中包含8个JavaScript脚本、8个CSS样式、1个HTML入口页面以及PNG、GIF、JPG等图片素材和README说明文档文件分工清晰整体仅415KB轻量易读。该资源已有118人学习下载适用于电商、社交媒体或企业活动中的抽奖领券场景。开发者可通过源码及附带说明学习移动端触控适配、手机号有效性校验、中奖概率分配等关键思路并能直接替换图片与文案二次开发成符合自身需求的营销工具。1. 手机验证抽奖领券的 HTML5 源码包到底能干什么做营销活动页的同行对“手机验证 抽奖 领券”这个组合肯定不陌生用户填手机号、等验证码、点一下转盘、页面弹出一张优惠券。这个 HTML5 源码包就是把这三步串成一套纯前端 demo 的现成工程——不需要你从零写转盘动画也不用先搭后端解压之后改改文案、调调参数就能塞进活动页当入口。我拿到这类源码做的第一件事从来不是打开 demo 看效果而是把它拆成三个独立模块弄明白“谁在管验证、谁在管转动、谁在管发券”。这套结构摸清楚了后面改颜色、改概率、接真实短信接口都是在原有骨架上换零件而不是黑匣子里瞎猜。2. 拆开源码包先看业务手机验证、抽奖、领券三个模块各管什么2.1 手机号验证模块格式校验和验证码倒计时的常见写法这个模块是整个活动页的“门禁”。源码包里最常见的实现不是真的调短信接口而是模拟了一个验证码发放流程手机号输入框绑input事件做实时校验点“获取验证码”按钮后前端生成一个随机四位数字连同手机号一起存到localStorage再进入倒计时状态。这样做的好处是脱离后端也能完整演示交互流程方便你直接打开页面就看到效果坏处也很明显——任何人都能通过控制台看到这个“验证码”。所以它在源码里是演示逻辑上线前必须替换成真实短信接口这一点后面单独说。// 获取验证码按钮的倒计时逻辑兼容页面刷新 const CODE_KEY sms_code; const LAST_SENT_KEY sms_last_time; document.querySelector(#sendCodeBtn).addEventListener(click, function () { const phone document.querySelector(#phoneInput).value; if (!/^1[3-9]\d{9}$/.test(phone)) { alert(手机号格式不对检查一下); return; } const secondsLeft getRemainSeconds(); if (secondsLeft 0) { alert(请 ${secondsLeft} 秒后再试); return; } const code String(Math.floor(1000 Math.random() * 9000)); // 模拟验证码 localStorage.setItem(CODE_KEY, code); localStorage.setItem(LAST_SENT_KEY, String(Date.now())); startCountDown(60); }); function getRemainSeconds() { const last Number(localStorage.getItem(LAST_SENT_KEY) || 0); return Math.max(0, 60 - Math.floor((Date.now() - last) / 1000)); } function startCountDown(total) { const btn document.querySelector(#sendCodeBtn); let remain total; btn.disabled true; btn.textContent ${remain}s 后重发; const timer setInterval(() { remain - 1; if (remain 0) { clearInterval(timer); btn.disabled false; btn.textContent 获取验证码; } else { btn.textContent ${remain}s 后重发; } }, 1000); }这里有几个参数值得留意。/^1[3-9]\d{9}$/是当前手机号校验的正则底线它覆盖了 13 到 19 开头的 11 位号码个别号段如果还没放号可以去掉前缀限制但不能放宽到 10 位或 12 位否则后面接短信接口时会被运营商侧直接打回。60是倒计时总时长很多活动页用 90 秒或 120 秒这个值越大用户等待感越强我一般保留 60。用Date.now()而不是单纯在内存里setInterval递减是为了防止用户切后台、刷新页面之后倒计时归零这个坑在下面排错章节还会展开。2.2 抽奖转盘模块核心动画是用 Canvas 还是 CSS transform翻源码时重点看转盘这一块它决定了整个页面的性能底子。老一点的做法是用 Canvas 把奖品扇形、颜色、文字直接画出来再让整块 Canvas 旋转近几年的移动端源码更多用transform: rotate配合 CSStransition因为 Canvas 在一倍屏和二倍屏之间需要做清晰度适配而 CSS 旋转不涉及重绘代码量也少得多。九宫格和转盘在实现上是两套思路九宫格靠操作一个活跃索引activeIndex做高亮移动转盘靠旋转度数停在某个扇区。这个标题里说的是“抽奖”常见源码以转盘居多下面按转盘讲。// 用一个角度值驱动转盘先确定中奖奖品再反算目标角度 const PRIZE_LIST [ { name: 5元券, ratio: 0.1 }, // 中奖率 10% { name: 谢谢参与, ratio: 0.4 }, { name: 2元券, ratio: 0.2 }, { name: 1元券, ratio: 0.3 } ]; const SECTOR_DEG 360 / PRIZE_LIST.length; let currentAngle 0; let isSpinning false; function spinOnce() { if (isSpinning) return; // 防止动画未结束又被点一次 isSpinning true; // 1. 按权重抽一个奖品索引 const idx weightedRandom(PRIZE_LIST); // 2. 让指针指向该扇区正中间 const targetAngle (360 - idx * SECTOR_DEG - SECTOR_DEG / 2 360) % 360; const finalAngle 360 * 5 360 - targetAngle; // 至少转5圈 const target document.querySelector(#wheel); target.style.transition transform 4s cubic-bezier(0.23, 1, 0.32, 1); target.style.transform rotate(${currentAngle finalAngle}deg); currentAngle finalAngle; target.addEventListener(transitionend, function handler() { isSpinning false; target.removeEventListener(transitionend, handler); }, { once: true }); }SECTOR_DEG是每个扇区占的角度4 个奖品就是 90 度6 个奖品就是 60 度改奖品数量时必须同步改这个值。360 * 5表示让转盘至少空转五圈视觉效果更饱满但要注意它叠加在currentAngle上如果一直累加不归零角度会变得非常大这时transition动画时长要相应加长否则会出现“转速突然加快”的错觉。weightedRandom是按ratio权重抽取索引的辅助函数常见做法是生成一个 0 到 1 的随机数然后累加权重区间判断落在哪段。真实活动里这个函数必须由服务端结果替代前端只负责播放动画防作弊的底线是“不能由前端定输赢”。2.3 领券弹窗模块领取状态与本地存储的配合转盘停下来之后页面需要给用户一个“到手”的反馈这就是领券模块。源码包里一般是一个居中的弹窗带券面金额、有效期、关闭按钮和“查看我的券”入口。它的真实职责有两件一是把中奖结果渲染到弹窗里二是记住“这台设备领过什么券”避免用户刷新页面之后又看到同一个弹窗。纯前端实现没有用户体系只能靠localStorage按手机号做 key 隔离。function drawCoupon(zipFileResult) { renderModal(zipFileResult); // 渲染弹窗样式 const phone document.querySelector(#phoneInput).value; const key coupon_${phone}; const claimed localStorage.getItem(key); if (claimed) { showAlreadyClaimed(claimed); // 已领过展示上次结果不再抽 } else { const coupon { id: Date.now(), name: zipFileResult.name, amount: zipFileResult.amount, expire: 2025-12-31 }; localStorage.setItem(key, JSON.stringify(coupon)); showSuccessModal(coupon); } }这个模块最值得改的是key的粒度。源代码如果只按手机号存就会出现“同一手机号换台手机还能继续领”的问题因为localStorage是跟着浏览器走的。要想严格一点就把手机号和设备指纹拼在一起当 key比如coupon_${phone}_${navigator.userAgent}。另外注意expire字段只是展示给用户看的文案真正能不能用要后端在下发时校验有效期。纯前端的领券在演示场景下没问题一旦上线必须保证“领券动作”请求的是后端接口由后端写数据库localStorage只做本地记忆。3. 在本地跑通这包源码解压、起服务、手机真机预览一条路走完3.1 解压后先看清目录结构别急着双击 index.html把 zip 解压之后第一步是按住Shift右键打开命令行输入tree或者用编辑器自带文件树瞄一眼结构。绝大多数 HTML5 活动页源码包长这样根目录一个index.htmlcss目录放样式js目录放业务逻辑img和audio放素材可能还有一个config.js集中管理奖品、倒计时时长、接口地址等配置项。文件结构越清晰改造越容易如果所有代码都躺在index.html的script里那就得做好逐行读代码的准备。解压后不要直接双击打开原因是很多源码用到了 ES6 模块或fetch请求本地jsonfile://协议下浏览器出于安全策略会直接拦截页面上表现为白屏或者控制台报错CORS。3.2 起一个本地静态服务让手机和电脑同网预览常见做法是用 Python 自带模块起服务Windows 和 macOS 都能直接跑。在项目根目录下执行下面的命令看到Serving HTTP on 0.0.0.0 port 8080就说明成了。这里监听0.0.0.0而不是127.0.0.1是为了让局域网里其他设备也能访问到。# 在解压出的项目根目录执行用 Python3 起静态服务 python3 -m http.server 8080 --bind 0.0.0.0然后另开一个终端查一下电脑的局域网 IP。macOS 在看网络设置里的“IP 地址”Windows 用ipconfig找IPv4 地址通常是192.168.x.x或10.x.x.x。再把手机和电脑连到同一个 Wi-Fi手机浏览器直接访问http://192.168.x.x:8080/就能看到页面。# Windows 查局域网 IP ipconfig | findstr IPv4 # macOS / Linux 查局域网 IP ifconfig | grep inet | grep -v 127.0.0.13.3 在手机上打开预览页的完整步骤手机预览时最容易忽略的是端口号被防火墙挡住。Windows 第一次跑http.server往往会弹“允许访问”对话框点“允许”之后手机才能连上。如果手机浏览器一直转圈先在电脑上自己访问http://localhost:8080/确认服务活着再排查防火墙最后确认手机和电脑确实在同一个网段。很多新手在这里翻车其实就是电脑开了访客网络或手机流量没关。进页面之后建议完整走一遍“输入手机号 → 获取验证码 → 模拟输入 → 抽奖 → 领券”的流程同时把手机浏览器开发者工具打开iOS 用 Safari 的“网页检查器”连接 Mac安卓用 Chromechrome://inspect看控制台报错。真机拿到的所有报错信息都要截屏记录下来后面排查参数时全靠它们。4. 把概率和参数改成自己的转盘权重、倒计时、动画时长的调法4.1 调整中奖概率从写死的随机数换成权重表源码包里最常见的概率实现是Math.random()直接乘奖品数量取整这种方式在奖品权重完全平均时能用但真实活动里“谢谢参与”的权重通常要压到 30% 到 50%就必须换成加权随机。找一个叫prizeList或ratios的数组改成下面的结构// 加权随机传入奖品数组返回奖品下标 function weightedRandom(prizes) { const total prizes.reduce((sum, p) sum p.ratio, 0); let random Math.random() * total; for (let i 0; i prizes.length; i) { random - prizes[i].ratio; if (random 0) return i; } return prizes.length - 1; }ratio不是百分比而是权重值四个奖品分别写0.1 / 0.4 / 0.2 / 0.3和1 / 4 / 2 / 3效果一样。我习惯把所有奖品配置抽到一个config.js里统一定义因为活动运营会频繁改权重放源码里他们没法下手。改权重时要记住前端权重只影响演示效果真正上线必须由后端返回中奖结果前端直接消费结果做动画否则用户改一下ratio就能把“谢谢参与”改成必中。4.2 改验证码倒计时和手机号校验规则倒计时时长和校验正则通常在同一个文件里搜索60和正则定位。把 60 改成 90 只需要改startCountDown(60)这一个入口即可但要注意getRemainSeconds里也写死了60两处不一致会出现“按钮显示 60 秒后重发但实际刚发出去就提示剩余 0 秒”的怪事。更省事的做法是把倒计时总时长提成一个常量const COUNT_DOWN_SECONDS 60; // 统一配置改成 90 只需动这里 const PHONE_REGEX /^1[3-9]\d{9}$/; // 手机号校验测试时可临时放开为 /^1\d{10}$/手机号正则不建议在测试阶段放开到/^1\d{10}$/因为现在所有校验规则最终要跟随真实短信接口接口会对号段做二次校验前端正则放宽只影响用户体验不影响安全。倒是新增表单标签这里值得注意很多老旧源码还在用input typetext接手机号正确的做法是把type改成tel或number手机上弹出的才是数字键盘这个体验差别极其明显。4.3 抽奖旋转圈数、时长与禁止重复点击的三个参数转盘的核心参数是旋转圈数、动画时长、缓动函数它们直接决定手感。圈数少显得小气圈数太多动画拖沓transition里配置 4 秒、5 圈、cubic-bezier(0.23, 1, 0.32, 1)是一组比较耐看的起点。施工时把参数抽到函数顶部方便在不改逻辑的情况下反复试手感const SPIN_CONFIG { rounds: 5, // 至少转 5 圈 duration: 4000, // 动画时长 4 秒 easing: cubic-bezier(0.23, 1, 0.32, 1), // 先快后慢 touchMask: true // 动画期间是否屏蔽点击 }; function spinOnce() { if (isSpinning) return; isSpinning true; if (SPIN_CONFIG.touchMask) { document.querySelector(#wheelBtn).disabled true; } // ...旋转逻辑transition 里的时长和缓动改为读 SPIN_CONFIG }isSpinning锁解决的是重复点击这个锁必须在transitionend里释放才能可靠工作。如果源代码用的是setTimeout解锁时间到了但动画还没走完用户快速点两次就会连续触发两次旋转第二次会覆盖第一次的目标角度表现成“转盘突然回头”。这是高频踩坑点在下一章的排查列表里详细说。5. 常见问题与避坑转盘停不准、真机白屏、验证码收不到5.1 转盘指针停不准动画结束回调里再算一次角度现象转盘动画结束后指针没有指向中奖奖品的正中偏了三分之一扇区或者每次停的位置都略有偏移。原因有两个一是目标角度算错把“指针要指向扇区正中”算成了“扇区起始边”二是用setTimeout释放锁没有和transitionend对齐。解决先确认扇区角度SECTOR_DEG和奖品顺序的对应关系再检查目标角度里是否加了SECTOR_DEG / 2这个“对中修正”。我惯用的写法是目标角度计算里整体加了一个360取模保证角度在 0 到 360 之间然后才叠加旋转圈数这样无论转多少圈最终停点都精确落在扇区中线。5.2 iOS 真机白屏或页面缩放异常viewport 与 safe-area现象同一个 HTML5 活动页安卓本地预览一切正常iPhone 打开白屏或者页面可以上下左右拖动字体被系统放大。原因源码漏了viewport元信息或者user-scalable写错iOS 自动调整字号也有关老网页在font-size小于 12px 时会强制放大。解决检查index.html的head里有没有下面这段meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno手机浏览器渲染环境对标签要求严格漏掉这行页面在桌面浏览器里看不出问题一到 iPhone 就按 980px 宽度渲染整个转盘布局被挤到屏幕外。做刘海屏适配时还要给领券弹窗加env(safe-area-inset-bottom)内边距防止底部按钮被 Home 条遮住。5.3 验证码按钮疯狂请求前端必须加的发送限制现象测试时连续点了十几次“获取验证码”每次都弹“已发送”页面也没有任何拦截。原因源码只写了倒计时disabled但倒计时状态没有持久化刷新页面立刻复活更严重的是没有做手机号维度限制同一个号码短时间收到多条短信。解决用前面讲的LAST_SENT_KEY记录时间戳刷新后读取计算剩余秒数同时在后端接口处限制同一手机号 60 秒只能发一次前端限制只是体验后端限制才是真门槛。纯前端演示包往往忽略这一点但上线前必须补否则短信服务账单会教你做人。5.4 抽奖结果和动画对不上先定奖品再定角度现象转盘转到“5 元券”弹窗却显示“谢谢参与”或者反过来。原因源码里角度计算顺序写反了——先随机一个角度再根据角度反推奖品这种写法在扇区角度不是整数时会错位而且角度随机不按权重走。解决严格按“先用加权随机抽出奖品索引 → 由奖品反算目标角度 → 播放动画”的顺序执行动画结束回调里读取的奖品就是之前定好的那个。这种顺序问题从代码上就能发现规律凡是动画函数里出现两次Math.random()的大概率有一个随机点在给动画用另一个在给奖品用。5.5 领券后刷新页面券没了localStorage 被清了还是存错了现象领完券刷新页面弹窗消失但打开“我的券”列表是空的或者领过券的手机号还能再领。原因localStorage存的时候用了临时变量比如把券信息塞进了会话级变量页面刷新就丢再或者存入的 key 每次生成都带随机数导致找不到上一次的记录。解决用固定格式的 key 如coupon_${phone}券内容用JSON.stringify存读取时再JSON.parse。写这段逻辑时顺手在控制台执行一次localStorage.key()查看所有存储项比对 key 是否一致排查效率直线上升。6. 继续向生产环境靠接口封装、埋点验证和防重复领取源码包演示到这里已经能跑通全流程但离上线还差三步。第一步是把验证码和领券这两个动作从前端假逻辑切换成真实接口。常见做法是封装一个api.js把sendSmsCode和drawCoupon定义成fetch请求前端只负责传手机号和展示状态返回码和校验全交给后端。中奖结果一定以接口返回的prizeId为准前端权重随机只留在演示环境或彩排版里。第二步是补埋点。活动页上线最怕“不知道用户卡在哪一步”至少要采集曝光、发送验证码、抽奖完成、领券成功四个节点。用navigator.sendBeacon做页面离开时的补发比XMLHttpRequest稳定不被跳转打断。埋点不是上线后的事改完代码立刻在本地用浏览器开发者工具验证一遍确认每个节点都有输出否则上线那天你会面对一个完全看不见用户行为的黑匣子。第三步是验证防重复领取和动画一致性。拿同一台手机连续抽三次确认第二次开始弹“已领取”换一个手机号再抽确认能正常领。然后开慢速网络模拟 4 秒左右动画拿秒表对着看转盘停止瞬间和弹窗出现时机。我个人的习惯是把“每次上线前用手机走一遍完整流程并录屏”写进发布 checklist录屏比截图可靠它能同时暴露动画掉帧、弹窗位置偏移、键盘遮挡输入框这些截图看不出的问题。这三个模块的源码包我接手过不止一次。每次改完参数我都会把config.js里的奖品数量、扇区角度、倒计时时长重新读一遍确认没有改一半漏一半——权重表改了但扇区数没改翻车最狠的就是这种小疏忽。如果你刚拿到这份 HTML5 源码包照着二三章的步骤先跑通再动第四章的参数最后回头检查第五章的坑半小时就能从“看不懂”到“随便改”。希望帮到你。本文还有配套的精品资源点击获取