ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

H5口红机源码:前端零后端抽奖组件,支持权重/保底/微信分享

H5口红机源码:前端零后端抽奖组件,支持权重/保底/微信分享

简介:H5口红机源码是一套面向Web前端与全栈开发者的互动游戏实战项目,适用于希望掌握HTML5游戏开发、移动端交互逻辑及前后端集成的中初级开发者。资源完整开源,涵盖游戏核心功能实现与可部署环境配置,助力用户快速复现、二次开发或教学演示。压缩包共含多个关键文件:含详细部署说明的Word文档(.docx)、初始化数据库结构与奖品数据的SQL脚本(.sql),以及主源码目录“全开源H5第五版”,内含HTML、JavaScript、CSS等标准H5技术栈代码,支持Canvas渲染、用户行为响应与动态视觉效果。资源包大小为30.52MB,结构清晰、模块分明,便于按需调试与功能扩展。目前已有369人学习下载,读者可直接获取可运行的游戏框架、理解H5游戏状态管理与数据库联动机制,并基于源码定制规则、奖品池或UI风格,是实践HTML5、JS逻辑控制与轻量级后端集成的优质学习样本。

1. H5口红机源码:一个轻量级互动营销组件,3分钟嵌入现有H5活动页,无需后端部署也能跑通抽奖逻辑

你刚收到市场部甩来的需求:“明天上线口红抽奖H5,用户分享3次得1次抽奖机会,中奖率可配置,要带粒子动画和微信分享回调”。你心里一咯噔——这种临时需求,通常意味着没接口、没文档、UI已定、时间只剩4小时。但这次,你点开「H5口红机源码」压缩包,解压后双击index.html,本地直接弹出口红旋转动画+点击触发音效+模拟中奖弹窗。它不是Demo,而是真实交付过5个品牌方活动的完整前端工程:所有抽奖策略(概率权重、保底机制、库存扣减)全在JS里跑,中奖结果用localStorage做轻量状态管理,分享回调走微信JS-SDK标准流程,连iOS Safari下wx.ready失效的兼容补丁都写好了。适合前端同学快速复用、运营同学自主改文案配图、小团队零后端依赖上线互动活动。如果你正被“又要快又要稳还要能改”压得喘不过气,这份源码就是你今晚能睡着的后悔药。


2. 项目结构与技术栈:Vue 2 + Webpack 4 构建,核心逻辑无框架依赖,老项目也能无缝接入

2.1 目录结构解析:6个关键文件夹讲清数据流闭环

项目采用分层清晰的目录组织,不追求炫技,只保证接手的人30秒内找到修改点:

/src ├── assets/ # 静态资源:口红SVG图标、粒子贴图、音效MP3(含静音开关逻辑) ├── components/ # 可复用UI:LipstickMachine(主组件)、PrizeModal(中奖弹窗)、ShareTip(分享引导浮层) ├── config/ # 运行时配置:prizeConfig.js(中奖规则表)、machineConfig.js(转盘参数)、apiMock.js(模拟后端返回) ├── libs/ # 独立工具库:spinEngine.js(物理级转盘动画引擎)、prizeLogic.js(抽奖核心算法) ├── utils/ # 辅助函数:wxShare.js(微信分享封装)、storage.js(localStorage增强版,支持过期时间) └── main.js # 入口:Vue实例初始化 + 微信JS-SDK注入 + 全局事件总线

提示:libs/prizeLogic.js是整个项目的“心脏”,后续章节会逐行拆解它的保底机制实现。它不依赖Vue,可直接在原生JS项目中import使用。

2.2 技术选型深意:为什么是Vue 2而不是Vue 3或React?

这不是技术债,而是精准匹配业务场景的决策:

  • Vue 2:活动页需兼容微信内置浏览器(X5内核),Vue 3的Proxy在部分安卓X5版本存在兼容问题,而Vue 2的Object.defineProperty方案稳定运行超2年;
  • Webpack 4:未升级到5,因SplitChunksPlugin对node_modules的默认分割策略更利于将vue、axios等基础库单独打包为vendor.js,首屏加载更快;
  • 零后端依赖:所有中奖逻辑在前端执行,config/apiMock.js模拟了真实API的响应结构(含code、msg、data字段),上线时只需替换utils/request.js中的baseURL即可对接真实接口。

2.3 核心模块依赖关系图(文字描述)

数据流向严格遵循单向流,避免状态污染:

用户点击 → LipstickMachine.vue → 调用 spinEngine.start() → ↓ spinEngine.js 计算旋转轨迹 → 触发 prizeLogic.roll() → ↓ prizeLogic.js 根据prizeConfig.js规则生成中奖ID → ↓ LipstickMachine.vue 接收ID → 渲染对应口红动画 + 调用ShareTip.show() → ↓ 用户点击“分享” → wxShare.js 封装分享参数 → 微信JS-SDK回调 → storage.set('shared', true)

注意:prizeLogic.roll()的返回值不直接等于中奖ID,而是经过prizeConfig.weightedMap映射后的最终结果,这是实现“10%概率中一等奖,85%概率中参与奖”的关键设计。

2.4 关键配置文件详解:改3个数字就能调中奖率

所有运营可配参数集中在/src/config/prizeConfig.js,无需动代码:

// /src/config/prizeConfig.js export default { // 【重点】中奖权重表:数值越大,中奖概率越高(自动归一化) weightedMap: [ { id: 'prize_1', weight: 10, name: 'YSL圣罗兰唇釉' }, // 占比约10% { id: 'prize_2', weight: 5, name: 'MAC子弹头口红' }, // 占比约5% { id: 'prize_3', weight: 85, name: '谢谢参与' } // 占比约85% ], // 保底机制:连续9次未中大奖(prize_1),第10次必中 guarantee: { triggerCount: 9, guaranteedPrizeId: 'prize_1' }, // 库存控制:前端校验,防止超发(真实场景需后端二次校验) inventory: { 'prize_1': 50, 'prize_2': 200, 'prize_3': -1 } // -1表示无限量 }

注意:inventory中的-1表示该奖品不限量,常用于“谢谢参与”类安慰奖。若需限制总量,填具体数字(如'prize_1': 50),前端会在每次抽奖前检查storage.get('prize_1_count')是否小于50。

2.5 构建与本地调试:npm run dev 启动即用,真机扫码实时预览

开发环境完全免配置,命令行一步到位:

# 安装依赖(仅首次) npm install # 启动本地服务(自动打开 http://localhost:8080) npm run dev # 打包生产环境(输出到 /dist,含Gzip压缩) npm run build

构建产物/dist目录结构极简:

/dist ├── index.html # 入口HTML,内联关键CSS,script标签引入打包JS ├── static/ │ ├── css/app.[hash].css # 提取的CSS,含动画关键帧 │ ├── js/app.[hash].js # 主逻辑(含Vue、抽奖引擎) │ └── js/vendor.[hash].js # 第三方库(vue、axios、weixin-js-sdk) └── favicon.ico

提示:npm run dev启动后,控制台会打印二维码,微信扫码即可在真机上实时调试——这是H5活动开发的刚需,源码已内置webpack-dev-server的https代理和disableHostCheck配置,避免真机访问时的跨域报错。


3. 核心抽奖逻辑拆解:prizeLogic.js 如何用纯JS实现保底+权重+库存三重校验

3.1roll()方法全流程:从随机数生成到最终中奖ID

这是整个口红机最核心的函数,位于/src/libs/prizeLogic.js。它不依赖任何框架,可独立测试:

// /src/libs/prizeLogic.js import config from '@/config/prizeConfig' import storage from '@/utils/storage' /** * 执行一次抽奖 * @returns {Object} { id: 'prize_1', name: 'YSL圣罗兰唇釉', isGuaranteed: false } */ export function roll() { const { weightedMap, guarantee, inventory } = config // 步骤1:检查保底机制(关键!) const lastRolls = storage.get('roll_history') || [] const recentGuaranteeHits = lastRolls.slice(-guarantee.triggerCount).filter(id => id === guarantee.guaranteedPrizeId) // 若最近triggerCount次中未出现guaranteedPrizeId,则本次强制中奖 if (recentGuaranteeHits.length === 0) { const result = { id: guarantee.guaranteedPrizeId, name: weightedMap.find(p => p.id === guarantee.guaranteedPrizeId)?.name || '大奖', isGuaranteed: true } // 更新历史记录(只存最近10次) const newHistory = [...lastRolls.slice(-9), result.id] storage.set('roll_history', newHistory) return result } // 步骤2:权重随机(标准加权随机算法) const totalWeight = weightedMap.reduce((sum, item) => sum + item.weight, 0) let random = Math.random() * totalWeight let cumulativeWeight = 0 for (const item of weightedMap) { cumulativeWeight += item.weight if (random <= cumulativeWeight) { // 步骤3:库存校验(前端轻量检查) const currentStock = storage.get(`${item.id}_count`) || 0 if (inventory[item.id] > 0 && currentStock >= inventory[item.id]) { // 库存已满,降级到下一个权重项(避免死循环,此处简化处理为重试1次) return roll() // 递归重试,实际项目建议设最大重试次数 } // 记录本次中奖 const newStock = currentStock + 1 storage.set(`${item.id}_count`, newStock) storage.set('roll_history', [...lastRolls.slice(-9), item.id]) return { id: item.id, name: item.name, isGuaranteed: false } } } }

逻辑说明与参数说明:

  • storage.get('roll_history')存储的是字符串数组(如['prize_3','prize_3','prize_1']),记录最近10次抽奖ID,用于保底判断;
  • guarantee.triggerCount: 9表示“连续9次未中prize_1,第10次必中”,这个数字在运营后台可动态下发,当前硬编码在配置中;
  • 权重计算采用累积权重法:生成0~totalWeight之间的随机数,遍历weightedMap累加weight,当随机数≤当前累积值时命中——这是最公平的加权随机,避免了Math.random()*100查表法的精度丢失;
  • 库存校验是前端友好型设计:storage.set(${item.id}_count, newStock)将中奖次数存入localStorage,配合inventory配置实现“前端限流”,真实项目必须在roll()返回后立即调用后端API扣减库存并校验,此处仅为演示逻辑闭环。

3.2spinEngine.js:物理感转盘动画的3个关键参数

转盘动画不是CSS3transform: rotate()简单旋转,而是模拟真实机械阻尼效果,核心在/src/libs/spinEngine.js:

// /src/libs/spinEngine.js export function start(prizeIndex, callback) { const $machine = document.querySelector('.lipstick-machine') const baseRotation = 360 * 5 // 基础旋转5圈,确保有“转起来”的感觉 const prizeAngle = 360 / config.weightedMap.length * prizeIndex // 目标奖品角度 const finalRotation = baseRotation + prizeAngle + getRandomOffset() // 加随机偏移,避免机械感 // CSS transition + transform 实现平滑旋转 $machine.style.transition = 'transform 4s cubic-bezier(0.34, 1.56, 0.64, 1)' $machine.style.transform = `rotate(${finalRotation}deg)` // 动画结束后触发回调(播放音效、显示弹窗) setTimeout(() => { callback && callback() }, 4000) } function getRandomOffset() { // ±15度随机偏移,让每次旋转终点略有不同 return (Math.random() - 0.5) * 30 }

参数说明:

  • cubic-bezier(0.34, 1.56, 0.64, 1)是自定义贝塞尔曲线,模拟“启动快→中途慢→结尾急停”的物理惯性,比ease-out更真实;
  • baseRotation = 360 * 5确保无论中哪个奖,转盘都至少转5圈,避免用户觉得“还没转就停了”;
  • getRandomOffset()添加±15度扰动,解决同一奖品多次中奖时动画重复的问题——这是用户反馈最多的“玄学体验点”。

3.3 微信分享闭环:wxShare.js 如何解决wx.ready不触发的黑匣子问题

微信分享在iOS Safari常因wx.config异步加载失败导致分享按钮无效,/src/utils/wxShare.js内置了三重保险:

// /src/utils/wxShare.js export function initShare(shareData) { // 第一重:延迟重试(微信JS-SDK加载有时延) setTimeout(() => { if (typeof WeixinJSBridge === 'undefined') { // 第二重:监听WeixinJSBridgeReady事件(iOS专属) document.addEventListener('WeixinJSBridgeReady', () => { setShare(shareData) }, false) return } // 第三重:直接执行(Android常见路径) setShare(shareData) }, 500) // 备用方案:若5秒后仍未ready,降级为页面链接分享 setTimeout(() => { if (!window.wxReady) { console.warn('微信分享初始化超时,启用降级方案') fallbackShare(shareData) } }, 5000) } function setShare(data) { window.wx.ready(() => { window.wx.onMenuShareTimeline(data) // 朋友圈 window.wx.onMenuShareAppMessage(data) // 聊天 window.wxReady = true }) window.wx.error((res) => { console.error('微信分享配置失败:', res) }) }

注意:window.wxReady是全局标志位,LipstickMachine.vue中通过watch监听此变量,仅当为true时才显示分享按钮,避免用户点击无响应。


4. 常见问题排查:5条血泪经验总结,覆盖90%线上翻车现场

4.1 现象:点击抽奖按钮无反应,控制台无报错

原因:微信JS-SDK未正确注入,window.wx为undefined,但LipstickMachine.vue中未做防御性判断,直接调用wx.ready()导致静默失败。
解决:在main.js入口处添加SDK加载检测:

// main.js 开头 if (typeof window !== 'undefined' && /MicroMessenger/i.test(navigator.userAgent)) { const script = document.createElement('script') script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js' script.onload = () => console.log('微信SDK加载成功') document.head.appendChild(script) }

4.2 现象:中奖后弹窗显示“undefined”,口红图片不加载

原因:prizeConfig.js中weightedMap的id与/src/assets/images/下的图片文件名不一致(如配置id: 'prize_1',但图片是prize1.png)。
解决:统一命名规范,在components/PrizeModal.vue中添加容错:

// PrizeModal.vue 的 computed prizeImage() { const id = this.prizeResult.id const validId = id.replace(/[^a-z0-9_]/gi, '') // 移除特殊字符 return require(`@/assets/images/${validId}.png`) }

4.3 现象:iOS真机上转盘动画卡顿,Android正常

原因:iOS Safari对transform: rotate()的硬件加速支持不稳定,尤其在<canvas>或复杂SVG叠加时。
解决:强制开启GPU加速,在/src/assets/css/app.css中添加:

.lipstick-machine { transform: translateZ(0); /* 触发GPU加速 */ backface-visibility: hidden; /* 防止iOS闪屏 */ }

4.4 现象:用户分享后,好友点击链接进入页面,抽奖次数未重置

原因:storage.get('roll_count')在不同用户间共享(localStorage是域名级),未按用户唯一标识隔离。
解决:改用URL参数传递用户ID,或使用sessionStorage(页面级隔离):

// utils/storage.js export function set(key, value) { // 改为 sessionStorage,确保每次新打开页面重置 sessionStorage.setItem(key, JSON.stringify(value)) }

4.5 现象:配置中将prize_1权重设为0,但仍有极低概率中奖

原因:权重归一化计算时,Math.random() * totalWeight的浮点精度误差导致边界值误判。
解决:在prizeLogic.js的权重循环中增加容错:

for (let i = 0; i < weightedMap.length; i++) { const item = weightedMap[i] if (item.weight <= 0) continue // 跳过权重为0的奖品 cumulativeWeight += item.weight if (random <= cumulativeWeight + 0.0001) { // 增加微小容差 // ...命中逻辑 } }

5. 二次开发实战:如何接入真实后端API并实现“中奖率动态下发”

5.1 替换模拟API:3步对接真实抽奖接口

真实项目中,prizeLogic.roll()必须与后端联动,不能仅靠前端计算。改造路径如下:

步骤1:定义后端API规范
要求后端提供POST /api/v1/lottery接口,请求体:

{ "userId": "wx_abc123", "activityId": "h5_lipstick_2024" }

响应体(必须与apiMock.js结构一致):

{ "code": 200, "msg": "success", "data": { "prizeId": "prize_1", "prizeName": "YSL圣罗兰唇釉" } }

步骤2:修改prizeLogic.js,注入request方法

// /src/libs/prizeLogic.js import request from '@/utils/request' // 已封装axios export async function roll(userId) { try { const res = await request.post('/api/v1/lottery', { userId }) if (res.code === 200) { // 前端仅做结果渲染,不参与逻辑计算 return { id: res.data.prizeId, name: res.data.prizeName, isGuaranteed: false // 后端应返回是否保底 } } else { throw new Error(res.msg) } } catch (err) { console.error('抽奖请求失败:', err) // 失败时降级到本地逻辑(保底+权重) return localRoll() // 调用原roll()函数 } }

步骤3:在LipstickMachine.vue中调用

// methods async handleSpin() { if (this.isSpinning) return this.isSpinning = true try { const result = await prizeLogic.roll(this.userId) // userId从URL或微信授权获取 this.showPrizeModal(result) } finally { this.isSpinning = false } }

5.2 中奖率动态下发:用localStorage实现配置热更新

运营需要随时调整中奖率,无需发版。方案是让前端定时拉取配置:

// /src/utils/configLoader.js export async function loadPrizeConfig() { try { const res = await request.get('/api/v1/config/prize?ts=' + Date.now()) if (res.code === 200) { // 覆盖本地配置 Object.assign(config, res.data) localStorage.setItem('prizeConfig', JSON.stringify(res.data)) console.log('中奖配置已更新') } } catch (err) { console.warn('配置拉取失败,使用本地缓存') const cached = localStorage.getItem('prizeConfig') if (cached) Object.assign(config, JSON.parse(cached)) } } // 在main.js中启动 setInterval(loadPrizeConfig, 5 * 60 * 1000) // 每5分钟拉一次

注意:/api/v1/config/prize返回的JSON结构必须与prizeConfig.js完全一致,包括weightedMap、guarantee、inventory字段。

5.3 埋点与数据看板:在关键节点插入GA或神策SDK

为评估活动效果,需在以下位置埋点:

节点事件名参数
用户进入页面page_view{ page: 'lipstick_machine' }
点击抽奖按钮click_spin{ userId: 'wx_abc123' }
中奖弹窗展示show_prize_modal{ prizeId: 'prize_1', isGuaranteed: false }
用户点击分享click_share{ shareType: 'timeline' }

在LipstickMachine.vue中实现:

// methods handleSpin() { this.$sensors.track('click_spin', { userId: this.userId }) // 神策SDK // ...其他逻辑 }, showPrizeModal(result) { this.$sensors.track('show_prize_modal', { prizeId: result.id, isGuaranteed: result.isGuaranteed }) }

从那以后我每次接到H5互动需求,第一件事就是解压这份口红机源码,删掉/src/config/apiMock.js,把prizeLogic.roll()改成async,再花10分钟配好后端地址——剩下的时间全是和UI抠动画细节。它不完美,但足够健壮;它不前沿,但足够省心。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进