# 快乐转盘 **Repository Path**: itcode-itcode/itcode-happy-wheel ## Basic Information - **Project Name**: 快乐转盘 - **Description**: 快乐转盘,微信小程序转盘自定义组件,可直接应用于微信小程序开发 - **Primary Language**: JavaScript - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-01 - **Last Updated**: 2026-06-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 快乐转一转转盘微信小程序 微信小程序转盘自定义组件实现方案 关键字:快乐,转盘,微信小程序,weixin,小程序,js,组件,微信组件,wxss 小程序转盘自定义组件实现方案,结合项目实际源码,将转盘封装为微信小程序自定义组件,适配多选项、动态数据、状态锁、动画、样式,同时兼容项目本地缓存数据。可以很方便的将组件代码引入微信小程序源码直接使用即可。零门槛非常友好。 如果你在开发转盘类小程序不妨来试试,玩一下,学习学习。 结合项目实际源码,将转盘封装为**微信小程序自定义组件**,适配多选项、动态数据、状态锁、动画、样式,同时兼容项目本地缓存数据。 # 一、项目目录结构 ```Plain Text zhuanpan/ ├── app.js # 应用入口 ├── app.json # 应用配置 ├── app.wxss # 全局样式 ├── project.config.json # 项目配置 ├── sitemap.json # 站点地图 ├── components/ │ └── wheel-view/ # 转盘组件 │ ├── wheel-view.js │ ├── wheel-view.json │ ├── wheel-view.wxml │ └── wheel-view.wxss ├── pages/ │ └── index/ # 首页 │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss └── utils/ ├── storage.js # 本地缓存工具 └── wheel.js # 转盘工具函数 ``` # 二、转盘组件代码 ## 1. wheel-view.json(组件配置) ```json { "component": true, "usingComponents": {} } ``` ## 2. wheel-view.wxml(组件结构) ```xml {{item.name}} GO 开始 本次结果:{{resultText}} ``` ## 3. wheel-view.wxss(组件样式 + 动画) ```css .wheel-box { display: flex; flex-direction: column; align-items: center; margin-top: 80rpx; position: relative; } .wheel-outer { position: relative; padding: 34rpx; border-radius: 50%; background: #E65100; } .wheel-main { position: relative; padding: 300rpx; border-radius: 50%; background: #EEC06B; transform-origin: center center; } .wheel-text { position: absolute; top: 50%; left: 50%; font-size: 32rpx; color: #333; font-weight: 500; transform-origin: 0 0; white-space: nowrap; } .wheel-line { height: 1rpx; background: #ffffff; position: absolute; top: 50%; left: 50%; transform-origin: 0 0; } .wheel-hole { width: 30rpx; height: 30rpx; border-radius: 50%; background: #fff; position: absolute; top: 50%; left: 50%; transform-origin: 0 0; } .wheel-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 999; } .wheel-triangle { position: absolute; left: 50%; top: -50rpx; transform: translateX(-50%) rotate(0deg); border-left: 20rpx solid transparent; border-right: 20rpx solid transparent; border-bottom: 60rpx solid #E65100; } .wheel-inner { padding: 20rpx; border-radius: 50%; background: #EEC06B; box-shadow: 0 5rpx 0 5rpx #C2793D; } .wheel-btn { width: 160rpx; height: 160rpx; line-height: 160rpx; text-align: center; border-radius: 50%; color: #fff; font-weight: bold; font-size: 32rpx; background: #FF7300; box-shadow: 0 5rpx 0 5rpx #C2793D; } .wheel-anim { animation: wheelSpin 5s cubic-bezier(0.17, 0.67, 0.13, 0.99) forwards; } @keyframes wheelSpin { 0% { transform: rotate(0deg); } 100% { transform: rotate(1800deg); } } .result-bar { width: 90%; margin-top: 40rpx; padding: 20rpx; text-align: center; background: #FF9EAA; border-radius: 50rpx; color: #fff; font-size: 30rpx; } .result-txt { color: #E65100; font-weight: bold; animation: blink 1s infinite; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } } ``` ## 4. wheel-view.js(组件逻辑、算法、事件) ```javascript Component({ properties: { options: { type: Array, value: [] } }, data: { viewWidth: 360, isRotating: false, holeList: [], lineList: [], optionList: [], resultText: '' }, lifetimes: { attached() { const sysInfo = wx.getWindowInfo() this.setData({ viewWidth: sysInfo.windowWidth }) this.initBaseData() } }, observers: { 'options': function (newVal) { if (Array.isArray(newVal) && newVal.length > 0) { this.initOptionAngle(newVal) } } }, methods: { calcAngle(count, offset = 0) { return Array.from({ length: count }, (_, idx) => { return { angle: (idx * 360 / count) + offset } }) }, initBaseData() { const holeList = this.calcAngle(16, -2.2) const lineList = this.calcAngle(8) this.setData({ holeList, lineList }) }, initOptionAngle(optionArr) { const total = optionArr.length const perAngle = 360 / total const optionList = optionArr.map((item, idx) => { return { ...item, angle: idx * perAngle + 25 } }) this.setData({ optionList, resultText: '' }) }, shuffleArray(arr) { const newArr = [...arr] for (let i = newArr.length - 1; i > 0; i--) { const randomIdx = Math.floor(Math.random() * (i + 1)) ;[newArr[i], newArr[randomIdx]] = [newArr[randomIdx], newArr[i]] } return newArr }, handleStart() { if (this.data.isRotating) return const { optionList } = this.data if (optionList.length < 2) { wx.showToast({ title: '请至少添加2个选项', icon: 'none' }) return } this.setData({ isRotating: true, resultText: '' }) const angleArr = optionList.map(item => item.angle) const nameArr = optionList.map(item => item.name) const newAngle = this.shuffleArray(angleArr) const newName = this.shuffleArray(nameArr) const newOptionList = newAngle.map((angle, idx) => { return { name: newName[idx], angle: angle } }) this.setData({ optionList: newOptionList }) setTimeout(() => { const target = newOptionList.find(item => item.angle === 250) const res = target ? target.name : '无结果' this.setData({ isRotating: false, resultText: res }) this.triggerEvent('rotateEnd', { result: res }) }, 5100) } } }) ``` # 三、工具函数 ## 1. utils/storage.js(本地缓存工具) ```javascript const STORAGE_PREFIX = 'wheel_' const STORAGE_KEY = { MY_WHEELS: STORAGE_PREFIX + 'my_wheels', CURRENT_WHEEL: STORAGE_PREFIX + 'current_wheel', HISTORY: STORAGE_PREFIX + 'history' } function init() { if (!get(STORAGE_KEY.MY_WHEELS)) { set(STORAGE_KEY.MY_WHEELS, []) } if (!get(STORAGE_KEY.CURRENT_WHEEL)) { set(STORAGE_KEY.CURRENT_WHEEL, {}) } if (!get(STORAGE_KEY.HISTORY)) { set(STORAGE_KEY.HISTORY, []) } } function get(key) { try { const value = wx.getStorageSync(key) return value !== '' ? value : null } catch (e) { console.error('storage.get error:', e) return null } } function set(key, value) { try { wx.setStorageSync(key, value) } catch (e) { console.error('storage.set error:', e) } } function remove(key) { try { wx.removeStorageSync(key) } catch (e) { console.error('storage.remove error:', e) } } function clear() { try { wx.clearStorageSync() } catch (e) { console.error('storage.clear error:', e) } } module.exports = { STORAGE_KEY, init, get, set, remove, clear } ``` ## 2. utils/wheel.js(转盘工具函数) ```javascript function calcAngle(count, offset = 0) { return Array.from({ length: count }, (_, idx) => { return { angle: (idx * 360 / count) + offset } }) } function shuffleArray(arr) { const newArr = [...arr] for (let i = newArr.length - 1; i > 0; i--) { const randomIdx = Math.floor(Math.random() * (i + 1)) ;[newArr[i], newArr[randomIdx]] = [newArr[randomIdx], newArr[i]] } return newArr } function initOptionAngle(optionArr) { const total = optionArr.length const perAngle = 360 / total return optionArr.map((item, idx) => { return { ...item, angle: idx * perAngle + 25 } }) } function getDefaultWheel() { return { id: 'wheel_default', title: '今天吃什么?', options: [ { name: '米饭', color: '#FF6B6B', weight: 1 }, { name: '面条', color: '#4ECDC4', weight: 1 }, { name: '炒菜', color: '#45B7D1', weight: 1 }, { name: '火锅', color: '#96CEB4', weight: 1 }, { name: '烧烤', color: '#FFEAA7', weight: 1 }, { name: '麻辣烫', color: '#DDA0DD', weight: 1 }, { name: '汉堡', color: '#98D8C8', weight: 1 }, { name: '寿司', color: '#F7DC6F', weight: 1 } ], skin: 'default', createTime: new Date().toLocaleDateString() } } module.exports = { calcAngle, shuffleArray, initOptionAngle, getDefaultWheel } ``` # 四、首页代码 ## 1. pages/index/index.json(页面配置) ```json { "navigationBarTitleText": "快乐转盘", "navigationBarBackgroundColor": "#FF8C38", "usingComponents": { "wheel-view": "/components/wheel-view/wheel-view" } } ``` ## 2. pages/index/index.wxml(页面结构) ```xml 快乐转盘 转动转盘,随机选择 ``` ## 3. pages/index/index.js(页面逻辑) ```javascript const storage = require('../../utils/storage.js') const wheelUtil = require('../../utils/wheel.js') Page({ data: { wheelOptions: [], currentWheel: {} }, onLoad() { this.loadCurrentWheelData() }, onShow() { this.loadCurrentWheelData() }, loadCurrentWheelData() { const currentWheel = storage.get(storage.STORAGE_KEY.CURRENT_WHEEL) const list = storage.get(storage.STORAGE_KEY.MY_WHEELS) const wheel = currentWheel && currentWheel.id ? currentWheel : (list && list.length > 0 ? list[0] : wheelUtil.getDefaultWheel()) if (!currentWheel || !currentWheel.id) { storage.set(storage.STORAGE_KEY.MY_WHEELS, [wheel]) storage.set(storage.STORAGE_KEY.CURRENT_WHEEL, wheel) } this.setData({ currentWheel: wheel, wheelOptions: wheel.options || [] }) }, onRotateEnd(e) { const { result } = e.detail const { currentWheel } = this.data this.saveHistoryRecord(result, currentWheel.title) wx.showToast({ title: '结果:' + result, icon: 'none', duration: 2000 }) }, saveHistoryRecord(result, title) { const history = storage.get(storage.STORAGE_KEY.HISTORY) || [] history.unshift({ id: 'history_' + Date.now(), type: 'wheel', title: title, result: result, time: new Date().toLocaleString() }) storage.set(storage.STORAGE_KEY.HISTORY, history.slice(0, 50)) } }) ``` # 五、核心特性说明 ## 1. 组件能力 - **解耦复用**:标准自定义组件,项目内任意页面可直接引用 - **动态数据**:通过 `options` 属性接收外部数据,选项变化自动刷新转盘 - **状态锁**:`isRotating` 禁止旋转过程重复点击,避免动画异常 - **随机逻辑**:基于洗牌算法打乱选项,固定 `250°` 为命中位置,结果随机可控 - **事件回调**:旋转结束通过 `rotateEnd` 事件向父组件回传结果,对接历史记录 - **全机型适配**:动态获取屏幕宽度,适配不同尺寸手机 ## 2. 动画参数 - 动画时长:`5s` - 旋转圈数:5 圈(`1800deg`) - 缓动函数:`cubic-bezier(0.17, 0.67, 0.13, 0.99)`,实现先加速后减速的自然惯性效果 - 结果动画:文字闪烁提醒,增强交互感知 ## 3. 默认数据 默认转盘主题为「今天吃什么?」,包含 8 个美食选项: - 米饭、面条、炒菜、火锅、烧烤、麻辣烫、汉堡、寿司 # 六、扩展修改方式 1. **修改选项数量**:修改 `options` 数组长度,组件自动均分角度 2. **调整旋转速度/圈数**:修改 `wheel-anim` 动画时长、`@keyframes` 内旋转角度 3. **修改配色**:编辑 `wheel-view.wxss` 中背景色、按钮色、指针色 4. **修改命中角度**:JS 中修改 `250` 这个固定角度值 # 七、常见问题排查 1. **转盘不刷新**:检查父页面 `options` 数据是否正常变化,组件 `observers` 监听已自动刷新 2. **可重复点击**:确认 `isRotating` 状态锁生效,动画期间状态为 `true` 3. **结果为空**:保证选项角度包含 `250°`,选项数量建议 8 个 4. **样式错位**:真机预览,rpx 单位已做移动端适配 --- > 文档版本:v1.0 > 生成时间:2026-06-01