sector.vue(扇形子组件)
html
<!--
简易扇形组件
-->
<template>
<view class="sector-component" :style="{
'--point-x': threePoint[0],
'--point-y': threePoint[1],
'--index-rotate': index * angle + 'deg',
'--bg-color': bgColor,
'--color': color,
'--size': size,
'--text-angle': angle / 2 - 45 + 'deg',
'--text-origin': radius + 'rpx',
'--text-translate-x-y': translateXY,
'--radius': -radius + 'rpx'
}">
<view class="text-wrap">
<view class="text">
<text class="title" v-if="text">
{{ text }}
</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'SimpleRotarySector',
props: {
// 文本内容
text: {
type: String,
default: '测试'
},
// 扇形角度
angle: {
type: Number,
default: 45
},
// 在整圈中的索引
index: {
type: Number,
default: 0
},
// 背景色
bgColor: {
type: String,
default: '#ff0000'
},
// 文字颜色
color: {
type: String,
default: '#ffffff'
},
// 字号
size: {
type: String,
default: '24rpx'
},
// 半径
radius: {
type: Number,
default: 300
}
},
computed: {
threePoint() {
if (this.angle < 0 || this.angle > 180) {
console.error('弧度值取值范围为0~180之间,当前值:' + this.angle);
return [0, 0];
}
const a = (this.angle <= 90 ? this.angle : 180 - this.angle) * (Math.PI / 180); // 角度转弧度
const r = this.radius;
let x = '0%';
let y = '0%';
if (this.angle > 90) {
y = '0%';
x = (((r + r * Math.cos(a)) / 2 / r) * 100).toFixed(2) + '%';
} else if (this.angle === 90) {
x = '50%';
y = '0%';
} else {
y = (((r - r * Math.sin(a)) / 2 / r) * 100).toFixed(2) + '%';
x = (((r - r * Math.cos(a)) / 2 / r) * 100).toFixed(2) + '%';
}
return [x, y];
},
translateXY() {
return (Math.sqrt(this.radius * this.radius * 2) - this.radius - 30).toFixed(0) + 'rpx';
}
}
};
</script>
<style lang="scss" scoped>
.sector-component {
width: 100%;
height: 100%;
background-color: var(--bg-color);
border-radius: 50%;
position: absolute;
left: 0;
top: 0;
clip-path: polygon(0 0, 0 50%, 50% 50%, var(--point-x) var(--point-y));
transform: rotate(calc(var(--index-rotate) + 90deg));
.text-wrap {
transform-origin: var(--text-origin) var(--text-origin);
// transform: rotate(var(--text-angle)) translateX(var(--text-translate-x-y)) translateY(var(--text-translate-x-y));
transform: rotate(var(--text-angle)) translateX(var(--text-translate-x-y)) translateY(var(--text-translate-x-y));
// border: 1px solid red;
}
.text {
font-size: var(--size);
color: var(--color);
padding: 0 10rpx;
height: 60rpx;
writing-mode: vertical-rl;
text-orientation: mixed;
display: flex;
align-items: center;
// justify-content: center;
transform: rotate(-45deg);
.title {
white-space: nowrap;
text-align: center;
letter-spacing: 2rpx;
}
}
}
</style>
simple-rotary-table.vue(主组件)
html
<!--
简易抽奖转盘组件(文字版)
-->
<template>
<view class="simple-rotary-table" :style="{
width: radius * 2 + 'rpx',
height: radius * 2 + 'rpx'
}">
<view class="content-table" :style="'-webkit-transform:rotate(' +
deg +
'deg) translateZ(0);transform:rotate(' +
deg +
'deg) translateZ(0)'
">
<sector v-for="(item, index) in displayList" :key="index" :radius="radius" :text="item.title"
:bgColor="item.bgColor" :color="item.color" :size="item.size" :angle="singleAngle" :index="index" />
</view>
<view class="turntable_pointer" @click="handleClick">
<!-- 静态资源:/static/imgs/begin-btn.png -->
<image src="/static/imgs/begin-btn.png" class="icon" />
</view>
</view>
</template>
<script>
import sector from './sector.vue';
export default {
name: 'SimpleRotaryTable',
components: { sector },
props: {
// 原始奖项列表
list: {
type: Array,
default: () => []
},
// 标题字段名,可配置,默认 title
labelKey: {
type: String,
default: 'title'
},
// 圆盘半径,单位 rpx
radius: {
type: Number,
default: 300
},
// 旋转速度
speed: {
type: Number,
default: 20
},
// 文字字号
fontSize: {
type: String,
default: '24rpx'
}
},
data() {
return {
deg: 0,
isStart: false,
timer: null,
// 中奖项,从 1 开始
awardNumer: 0,
// 中奖项数据
awardData: {},
// 每个扇区的随机背景色
segmentColors: []
};
},
computed: {
// 每个扇区角度
singleAngle() {
const len = this.displayList.length || 1;
return Number((360 / len).toFixed(2));
},
// 中奖结束角度
endAddAngle() {
return 360 - ((this.awardNumer - 1) * this.singleAngle + this.singleAngle / 2);
},
// 用于渲染的列表:只显示文字,自动填充随机背景色和白色字体
displayList() {
return this.list.map((item, index) => {
const titleKey = this.labelKey || 'title';
const text = item[titleKey] != null ? String(item[titleKey]) : '';
const bgColor = item.bgColor || this.segmentColors[index] || this.getRandomColor();
const color = item.color || '#ffffff';
const size = item.size || this.fontSize;
return {
title: text,
bgColor,
color,
size
};
});
}
},
watch: {
list: {
handler() {
this.initSegmentColors();
},
deep: true,
immediate: true
}
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
methods: {
// 初始化随机颜色,保证每个块颜色尽量不一样
initSegmentColors() {
const len = this.list.length || 0;
if (!len) {
this.segmentColors = [];
return;
}
// 一组预置的高对比色,随机打乱后依次取用,不够再随机补充
const baseColors = [
'#F56C6C',
'#E6A23C',
'#67C23A',
'#409EFF',
'#909399',
'#F0A1A8',
'#9B59B6',
'#1ABC9C',
'#2ECC71',
'#3498DB',
'#E74C3C',
'#F39C12'
];
const shuffled = baseColors.slice().sort(() => Math.random() - 0.5);
const colors = [];
for (let i = 0; i < len; i++) {
if (i < shuffled.length) {
colors.push(shuffled[i]);
} else {
colors.push(this.getRandomColor());
}
}
this.segmentColors = colors;
},
// 生成一个随机背景色
getRandomColor() {
const r = Math.floor(Math.random() * 200);
const g = Math.floor(Math.random() * 200);
const b = Math.floor(Math.random() * 200);
return `rgb(${r}, ${g}, ${b})`;
},
// 点击中心按钮:开始旋转并抛出事件,外部去调用接口拿中奖 id
handleClick() {
if (this.isStart || !this.displayList.length) return;
this.$emit('start');
// this.begin();
},
// 圆盘正式转起来
begin() {
if (this.isStart || !this.displayList.length) return;
this.isStart = true;
this.turnRound();
},
turnRound() {
let beginDeg = this.deg;
const beginSpeed = this.speed;
const rangeAngle = (Math.floor(Math.random() * 4) + 4) * 360; // 随机旋转几圈再停止
let cAngle;
beginDeg = 0;
let waitDeg = 0; // 等待接口返回前转的角度
this.timer = setInterval(() => {
if (!this.awardNumer) {
beginDeg += beginSpeed;
waitDeg = beginDeg;
} else {
if (waitDeg) {
beginDeg = 0;
waitDeg = 0;
}
if (beginDeg < rangeAngle) {
beginDeg += beginSpeed;
} else {
const a = (this.endAddAngle + rangeAngle - beginDeg) / beginSpeed;
cAngle = a > beginSpeed ? beginSpeed : a < 1 ? 1 : a;
beginDeg += cAngle;
if (beginDeg >= this.endAddAngle + rangeAngle) {
beginDeg = this.endAddAngle + rangeAngle;
this.isStart = false;
clearInterval(this.timer);
this.timer = null;
this.$emit('end', this.awardData);
}
}
}
this.deg = beginDeg;
}, 1000 / 60);
},
/**
* 停止转盘,number 为中奖序号(从 1 开始,对应传入 list 的第几个)
*/
stop(id) {
// console.log('列表数据:', this.list, id);
const index = this.list.findIndex(item => item.id == id)
// console.log('中奖序号:', index);
this.awardNumer = index + 1;
this.awardData = this.list[index]
},
/**
* 立即停止转盘,指针回到默认开始位置,不选择任何块
* 用于接口报错时的处理
*/
stopReset() {
this.awardNumer = 0;
this.awardData = {};
this.isStart = false;
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.deg = 0;
}
}
};
</script>
<style scoped lang="scss">
.simple-rotary-table {
border-radius: 50%;
border: 1px solid #999;
position: relative;
background-color: #f5f5f5;
overflow: hidden;
}
.content-table {
width: 100%;
height: 100%;
}
.turntable_pointer {
position: absolute;
width: 180rpx;
height: 180rpx;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
.icon {
width: 100%;
height: 100%;
}
}
</style>
简易抽奖转盘组件(文字版)
组件目录:
components/simple-rotary-table/组件文件:
simple-rotary-table.vue(主组件)、sector.vue(扇形子组件)
该组件为纯文字抽奖转盘 ,实现参考了现有转盘逻辑,但为全新实现,不依赖 mosowe-rotary-table-compatible 目录下的任何组件。
每个扇区使用随机背景色、白色文字,文字在扇区内居中展示。
功能说明
- 动态扇区数量:根据传入奖项列表长度,自动计算每个扇区角度。
- 随机背景色 :为每个扇区分配高对比随机背景色,也可由外部指定
bgColor。 - 白色文字居中:文字在扇区内居中显示,仅展示文字,不显示图片。
- 字段名可配置 :通过
labelKey指定要显示的字段,默认使用title。 - 中间按钮开始抽奖 :中间使用
static/imgs/begin-btn.png作为按钮图片,点击后触发start事件。 - 等待接口返回中奖 id 再停 :组件内部先持续旋转,外部获取中奖数据后调用
stop(data),转盘缓动到对应扇区。 - 接口异常处理 :提供
stopReset()方法,用于接口报错时重置转盘状态。
Attributes
主组件:simple-rotary-table.vue
| 属性 | 说明 | 类型 | 默认 |
|---|---|---|---|
| list | 奖项列表 | any[] | [] |
| labelKey | 用于显示文字的字段名 | string | 'title' |
| radius | 圆盘半径,单位 rpx | number | 300 |
| speed | 转盘旋转速度 | number | 20 |
| fontSize | 扇区中文字字号,单位 rpx | string | '24rpx' |
listItem 建议结构
组件只依赖文字字段,其他字段可按业务需要自定义,例如:
js
{
name: '一等奖', // 或自定义字段,通过 labelKey 指定
id: 1, // 业务字段,用于 stop 时匹配
bgColor: '#F56C6C', // 可选,如不传则内部生成随机色
color: '#ffffff', // 可选,文字颜色,默认白色
size: '24rpx' // 可选,文字大小
}
Events
| Events | 说明 | 回调参数 |
|---|---|---|
| start | 点击中间抽奖按钮时触发(需外部调用 begin) | - |
| end | 转盘完全停止时触发 | awardData(中奖项完整数据) |
方法
通过 ref 调用主组件:
| 方法名 | 说明 |
|---|---|
| begin | 手动开始转动(点击按钮后需手动调用此方法开始旋转) |
| stop(data) | 停止转动并停在指定奖项,参数为中奖项的 id 或数据对象 |
| stopReset | 立即停止转盘,指针回到默认开始位置(用于接口报错) |
stop(data: object | number)
-
data:中奖项数据对象或 id- 传入对象:组件会根据
id字段在list中查找对应奖项 - 传入 id:直接根据 id 在
list中查找对应奖项
- 传入对象:组件会根据
-
示例:
js// 方式1:传入完整数据对象 this.$refs.simpleRotaryRef.stop({ id: 1, name: '一等奖' }) // 方式2:只传入 id this.$refs.simpleRotaryRef.stop(1)
stopReset()
-
无参数,用于接口调用失败时重置转盘状态
-
转盘会停止并回到初始位置(角度为 0)
-
示例:
jsthis.$refs.simpleRotaryRef.stopReset()
使用示例
html
<template>
<view class="page-lottery">
<simple-rotary-table
ref="simpleRotaryRef"
:list="prizeList"
labelKey="name"
:radius="260"
:speed="20"
@start="start"
@end="end"
/>
</view>
</template>
<script>
import SimpleRotaryTable from "@/components/simple-rotary-table/simple-rotary-table.vue";
export default {
components: { SimpleRotaryTable },
data() {
return {
prizeList: [
{ name: "一等奖", id: 1 },
{ name: "二等奖", id: 2 },
{ name: "三等奖", id: 3 },
{ name: "谢谢参与", id: 4 },
],
};
},
methods: {
// 点击中间"开始抽奖"按钮后触发
async start() {
// 检查活动状态和次数等
if (!this.activityDetail.status) {
uni.showToast({ title: '活动已结束' })
return
}
if (!this.activityDetail.remainRewordCount) {
uni.showToast({ title: '抽奖次数已用完' })
return
}
// 开始转盘
this.$refs.simpleRotaryRef.begin();
// 调用接口获取中奖信息
try {
const res = await lottery({ id: this.activityId });
if (res.code == '1') {
// 接口返回中奖数据,停止转盘
this.$refs.simpleRotaryRef.stop(res.data);
}
} catch (err) {
// 接口报错,重置转盘
console.log('报错停止转盘', err);
this.$refs.simpleRotaryRef.stopReset();
}
},
// 转盘完全停止
end(e) {
// e 为中奖项的完整数据对象
uni.showToast({
title: e.name, // 或 e.name,取决于 labelKey 配置
icon: 'none'
});
},
},
};
</script>
工作流程
- 用户点击中间抽奖按钮
- 触发
@start事件 - 外部检查业务逻辑(活动状态、抽奖次数等)
- 调用
begin()方法开始转盘旋转 - 同时请求抽奖接口获取中奖结果
- 接口返回后,调用
stop(data)传入中奖数据 - 转盘缓动停止在对应扇区
- 触发
@end事件,返回中奖项完整数据
异常处理
| 场景 | 处理方式 |
|---|---|
| 接口调用失败 | 调用 stopReset() 重置转盘 |
| 活动已结束 | @start 事件中拦截,不调用 begin |
| 抽奖次数已用完 | @start 事件中拦截,不调用 begin |
| 中奖数据 id 不匹配 | 转盘会在最后一个扇区停止 |
子组件:sector.vue(说明)
sector.vue 为内部扇形绘制组件:
- 通过
angle、index、radius计算 clip-path,绘制不同数量的扇形块。 - 使用
bgColor控制每个扇区的背景色。 - 文本通过
text、color、size控制,居中显示在对应扇区内。
外部一般无需直接使用 sector.vue,只需要使用主组件 simple-rotary-table.vue 即可。
注意事项
- 点击按钮 ≠ 开始旋转 :
@start事件只是触发,需要外部调用begin()才会真正旋转 - stop 方法参数 :可以传入完整对象或 id,组件会自动匹配
list中的数据 - stopReset 使用场景 :仅在接口异常时调用,正常流程使用
stop(data)停止 - 扇区数量:建议不少于 2 个,否则转盘显示异常