# 快乐转盘
**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