微信小程序实现转盘抽奖,可以自定义编辑奖项列表

这个功能可以分几步实现:

1. 界面设计:

转盘区域: 使用 canvas 绘制转盘,可配置扇形数量、颜色、文字等。

按钮: "开始/停止" 按钮控制转盘转动。

编辑按钮: 点击弹出弹窗,编辑转盘项目。

中奖弹窗: 显示中奖结果。

2. 数据结构:

使用数组存储转盘项目数据,例如:

const prizeList = [

{ name: '一等奖', color: '#FFD700' },

{ name: '二等奖', color: '#C0C0C0' },

{ name: '三等奖', color: '#CD7F32' },

// ... 其他奖项

];

3. 功能实现:

绘制转盘:

根据 prizeList 数据计算每个扇形的角度。

使用 canvas API 绘制扇形、文字、边框等。

drawPrizeWheel() {

// 创建离屏 2D canvas 实例,创建canvas元素

const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 300});

// 获取 context。注意这里必须要与创建时的 type 一致const ctx = canvas.getContext('2d');

const centerX = 150;

const centerY = 150;

const radius = 130;

const prizeCount = this.data.prizeList.length;

const anglePerItem = 360 / prizeCount;

// 记录当前角度,初始为 0/90 度

let currentAngle = 90;

for (let i = 0; i < prizeCount; i++) {

ctx.beginPath();

ctx.moveTo(centerX, centerY);

ctx.arc(

centerX,

centerY,

radius,

(i * anglePerItem * Math.PI) / 180,

((i + 1) * anglePerItem * Math.PI) / 180,

);

ctx.closePath();

ctx.fillStyle = this.data.prizeList[i].color;

ctx.fill();

// --- 绘制文字 ---

ctx.save();

// 保存当前画布状态

ctx.font = '12px sans-serif';

ctx.fillStyle = '#fff';

ctx.textAlign = 'center';

ctx.textBaseline = 'middle';

const textAngle = (i * anglePerItem + anglePerItem / 2) * (Math.PI) / 180;

const textX = centerX + radius * 0.7 * Math.cos(textAngle);

const textY = centerY + radius * 0.7 * Math.sin(textAngle);

ctx.fillText(this.data.prizeList[i].name, textX, textY);

ctx.restore();

// 恢复之前的画布状态// --- 文字绘制结束 ---

// 计算当前奖项区域的结束角度

let endAngle = currentAngle + anglePerItem;

if(endAngle > 360){

endAngle %= 360;

}

// 计算当前奖项区域的起始角度和结束角度

// 将角度信息存储到 prizeList 数组中

this.data.prizeList[i].startAngle = currentAngle;

this.data.prizeList[i].endAngle = endAngle;

// 更新 currentAngle,准备绘制下一个区域

currentAngle = endAngle;

}

// 将canvas转换为DataURL格式的图片var dataURL = canvas.toDataURL('image/png', 1);this.setData({wheelImg: dataURL});

},

代码解释:

初始化currentAngle: 在循环开始之前,将currentAngle初始化为 90,表示从 90 度开始绘制。因为canvas 绘制圆弧的起始角度是水平向右的 x 轴正方向,而不是竖直向上的 y 轴正方向。所以这里实际起始角度是从90度开始。

计算结束角度: 在每次循环中,根据currentAngle和anglePerItem计算当前奖项区域的结束角度endAngle。

绘制扇形: 使用currentAngle和endAngle绘制当前奖项区域的扇形。最后把canvas转换为base64图片地址。

记录角度信息: 将currentAngle和endAngle存储到prizeList数组中,方便后续判断中奖区域。

更新currentAngle: 将currentAngle更新为endAngle,以便绘制下一个奖项区域。

记录角度信息: 在drawPrizeWheel方法中,我们为每个奖项对象添加了startAngle和endAngle属性,用来存储该奖项区域的起始和结束角度。

判断指针位置: 在stopRotate方法中,我们循环遍历prizeList数组,并根据每个奖项的startAngle和endAngle判断指针是否落在该区域内。

注意处理了跨越 0 度的情况,如果startAngle大于endAngle,则表示该区域跨越了 0 度,需要分别判断指针是否大于等于startAngle或者小于endAngle。

通过以上修改,奖项区域将从 0 度开始绘制,并且每个区域的角度信息会被正确记录在prizeList数组中,然后在stopRotate方法中准确判断指针落在哪个区域,从而确定中奖结果。

转盘转动:

使用setInterval/setTimeout或requestAnimationFrame实现动画效果。

控制转速和停止位置。

<view class="pointer" style="transform: rotate({{pointerAngle}}deg)"></view>

//开始旋转

startRotate() {

if (this.data.isRotating) return;

this.setData({ isRotating: true });

// 生成随机旋转圈数(至少旋转 6 圈)

const randomRounds = 6 + Math.floor(Math.random() * 3);

// 生成随机停止角度

const finalAngle = Math.floor(Math.random() * 360);

// 计算总旋转角度

const totalRotation = randomRounds * 360 + finalAngle;

// 使用 setInterval 实现动画

const startTime = Date.now();const frameRate = 80;

const rotateAnimation = () => {

const currentTime = Date.now();

const elapsed = currentTime - startTime;

const progress = Math.min(elapsed / this.data.animationDuration, 1);

// 使用 ease-out 动画曲线

const easeOut = (t) => 1 - Math.pow(1 - t, 3);

// 计算当前角度

const currentAngle = easeOut(progress) * totalRotation;

this.setData({ pointerAngle: currentAngle });

if (progress < 1) {

setTimeout(rotateAnimation, 1000 / frameRate);

} else {

this.stopRotate();

}

};

rotateAnimation();

},

代码解释:

随机旋转圈数: 在startRotate方法中,我们使用randomRounds变量来生成一个随机的旋转圈数,至少 3 圈,最多 6 圈。

随机停止角度: 使用finalAngle变量生成一个 0 到 360 之间的随机角度,表示指针最终停止的位置。

计算总旋转角度: 将旋转圈数转换为角度,再加上最终停止角度,得到总旋转角度totalRotation。

使用setInterval/setTimeout实现动画: 使用setInterval/setTimeout按照帧率更新指针角度,并使用easeOut动画曲线使指针旋转更自然。

停止动画和判断中奖区域: 在stopRotate方法中,根据最终指针角度finalAngle计算中奖区域索引,并弹出中奖信息。

旋转指针: wxml 中使用style="transform: rotate({{pointerAngle}}deg)"控制指针旋转,pointerAngle存储指针旋转角度。

指针旋转中心: wxss 中设置.pointer的transform-origin: 50% 100%;将旋转中心点设置为指针底部。

stopRotate方法: 修改逻辑,计算指针需要旋转到的角度targetAngle,并更新pointerAngle数据。

现在,转盘指针将会随机旋转几圈,然后随机停止在某个角度,并显示指针指向的奖项作为中奖结果。

中奖判断:

根据停止位置计算中奖索引。

显示中奖弹窗,展示 prizeList 中对应的数据。

stopRotate() {

let winningIndex = null;

const prizeCount = this.data.prizeList.length;

const anglePerItem = 360 / prizeCount;

const finalAngle = this.data.pointerAngle;

const pointerAngle = (finalAngle) % 360;

// 循环遍历每个奖项区域,判断指针是否落在该区域内

for (let i = 0; i < prizeCount; i++) {

const { startAngle, endAngle } = this.data.prizeList[i];

// 处理跨越 0 度的情况,跨越0度就是该区域的开始角度大于结束角度,如340-20

if (startAngle > endAngle) {

if (pointerAngle >= startAngle || pointerAngle < endAngle) {

winningIndex = i;

break;

}

} else {

if (pointerAngle >= startAngle && pointerAngle < endAngle) {

winningIndex = i;

break;

}

}

}

this.setData({

isRotating: false

});

const winningPrize = this.data.prizeList[winningIndex].name;

wx.showModal({

title: "恭喜!",

content: `您获得了${winningPrize}!`,

showCancel: false,

});

},

代码解释:

获取旋转后的角度: finalAngle 表示指针旋转后的随机角度

计算中奖索引:将 finalAngle取余 360,得到相对于第一个奖项区域起始位置的角度pointerAngle 。

遍历奖项区域: 循环遍历每个奖项,计算每个奖项区域的起始角度startAngle和结束角度endAngle。

判断指针位置: 判断pointerAngle是否落在当前奖项区域内 (startAngle到endAngle之间)。

确定中奖索引: 如果指针落在某个奖项区域内,记录下该奖项的索引winningIndex,并跳出循环。

处理结果: 根据winningIndex获取中奖信息,并进行后续处理。如果winningIndex为null,则表示出现异常,需要进行相应的处理。

注意:

(startAngle到endAngle之间)存在跨越0度的情况,要进行判断

如果你的奖项区域绘制顺序或方向与示例不同,你需要相应地调整计算逻辑。

希望这个解决方案可以帮助你!

编辑转盘:

弹窗中使用列表展示 prizeList 数据,可以进行增删改操作。

修改 prizeList 数据后,重新绘制转盘。

editPrize() {

this.setData({

showModal: true

});

},

closeModal() {

this.setData({

showModal: false

});

},

addNewPrize() {

const colorsLength = this.data.colorsList.length;

const random = Math.floor(Math.random() * colorsLength);

this.setData({

prizeList: [...this.data.prizeList, {

name: '',

color: this.data.colorsList[random] || '#000000'

}],

});

},

deletePrizeItem(e) {

const index = e.currentTarget.dataset.index;

let updatedPrizeList = [...this.data.prizeList];

updatedPrizeList.splice(index, 1); // 从数组中移除对应奖项this.setData({

prizeList: updatedPrizeList,

});

},

updatePrizeName(e) {

const index = e.currentTarget.dataset.index;

const value = e.detail.value;

let updatedPrizeList = this.data.prizeList;

updatedPrizeList[index].name = value;

this.setData({

prizeList: updatedPrizeList,

});

},

updatePrizeColor(e) {

const index = e.currentTarget.dataset.index;

const color = e.detail.value;

let updatedPrizeList = this.data.prizeList;

updatedPrizeList[index].color = color;

this.setData({

prizeList: updatedPrizeList,

});

},

savePrizeList() {

// 将 prizeList 转换为 JSON 字符串并存储// 这里可以根据你的需求修改存储方式const prizeListStr = JSON.stringify(this.data.prizeList);

// console.log("保存的 JSON 字符串:", prizeListStr);this.closeModal();

this.drawPrizeWheel(); // 重新绘制转盘

},

4. 代码示例:

index.wxml

<view class="container"><view class="wheel-container">

<view class="prize-wheel">

<image class="canvas" src="{{wheelImg}}"></image></view><view class="pointer" style="transform: rotate({{pointerAngle}}deg)"></view>

</view><button class="btn" bindtap="startRotate" disabled="{{isRotating}}">开始</button><button class="btn edit-btn" bindtap="editPrize">编辑</button>

<modal title="编辑奖项" hidden="{{!showModal}}" bindcancel="closeModal">

<view class="edit-area">

<!-- <view class="color-picker-slider">

<view>R: <slider min="0" max="255" value="{{r}}" bindchange="onColorSliderChange" data-channel="r"/></view>

<view>G: <slider min="0" max="255" value="{{g}}" bindchange="onColorSliderChange" data-channel="g"/></view>

<view>B: <slider min="0" max="255" value="{{b}}" bindchange="onColorSliderChange" data-channel="b"/></view>

<view class="color-preview" style=" color: rgb(189, 99, 197);">{{r}}, {{g}}, {{b}});"></view>

</view> -->

<view class="prize-item" wx:for="{{prizeList}}" wx:key="index"><input

class="prize-name"

placeholder="奖项名称"

value="{{item.name}}"

data-index="{{index}}"bindinput="updatePrizeName"

/><input

class="color-picker"

type="color"

value="{{item.color}}"

data-index="{{index}}"bindchange="updatePrizeColor"

/>

<button class="delete-btn" data-index="{{index}}" bindtap="deletePrizeItem">-</button>

</view>

</view>

<button class="btn add-btn" bindtap="addNewPrize">+</button><button class="btn save-btn" bindtap="savePrizeList">保存</button>

</modal>

</view>

index.js

Page({

data: {

prizeList: [{

name: '一等奖',

color: '#FFD700'

},

{

name: '谢谢参与',

color: '#C0C0C0'

},

{

name: '二等奖',

color: '#CD7F32'

},

{

name: '谢谢参与',

color: '#C0C0C0'

},

{

name: '三等奖',

color: '#A0522D'

},

{

name: '谢谢参与',

color: '#C0C0C0'

},

],

prizeListStr: '',

showModal: false,

rotateAngle: 0, // 用于模拟转盘动画,实际并未使用animation: null, // 未使用pointerAngle: 0, // 指针角度isRotating: false, // 是否正在旋转animationDuration: 5000, // 动画持续时间 (毫秒)showColorPickerVisible: false,

currentPrizeIndex: null,

r: 0,

g: 0,

b: 0,

colorsList: [

'#FFD700', '#C0C0C0', '#CD7F32', '#A0522D', '#DC143C', '#FF69B4',

'#BA55D3', '#7B68EE', '#6A5ACD', '#483D8B', '#4682B4', '#00CED1',

'#5F9EA0', '#2E8B57', '#9ACD32', '#FFFF00', '#FFA500', '#FF4500',

'#8B4513', '#D2691E', '#B8860B', '#808000', '#556B2F', '#228B22',

'#008000', '#006400', '#90EE90', '#00FF7F', '#00FA9A', '#20B2AA',

'#235788', '#697656', '#D935E9', '#B69754', '#BFA476', '#BAC289',

'#983471', '#EA4586', '#EABCDE', '#ACBEAD', '#825673', '#65C946',

],

},

onLoad() {

this.drawPrizeWheel();

},

onReady() {

},

onColorSliderChange(e) {

const channel = e.currentTarget.dataset.channel;

const value = e.detail.value;

this.setData({

[channel]: value

});

},

drawPrizeWheel() {

// 创建离屏 2D canvas 实例,创建canvas元素const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 300});

// 获取 context。注意这里必须要与创建时的 type 一致const ctx = canvas.getContext('2d');

const centerX = 150;

const centerY = 150;

const radius = 130;

const prizeCount = this.data.prizeList.length;

const anglePerItem = 360 / prizeCount;

// 记录当前角度,初始为 0/90 度let currentAngle = 90;

for (let i = 0; i < prizeCount; i++) {

ctx.beginPath();

ctx.moveTo(centerX, centerY);

ctx.arc(

centerX,

centerY,

radius,

(i * anglePerItem * Math.PI) / 180,

((i + 1) * anglePerItem * Math.PI) / 180,

);

ctx.closePath();

ctx.fillStyle = this.data.prizeList[i].color;

ctx.fill();

// --- 绘制文字 ---

ctx.save(); // 保存当前画布状态

ctx.font = '12px sans-serif';

ctx.fillStyle = '#fff';

ctx.textAlign = 'center';

ctx.textBaseline = 'middle';

const textAngle = (i * anglePerItem + anglePerItem / 2) * (Math.PI) / 180;

const textX = centerX + radius * 0.7 * Math.cos(textAngle);

const textY = centerY + radius * 0.7 * Math.sin(textAngle);

ctx.fillText(this.data.prizeList[i].name, textX, textY);

ctx.restore(); // 恢复之前的画布状态// --- 文字绘制结束 ---

// 计算当前奖项区域的结束角度let endAngle = currentAngle + anglePerItem;

if(endAngle > 360){

endAngle %= 360;

}

// 计算当前奖项区域的起始角度和结束角度// 将角度信息存储到 prizeList 数组中this.data.prizeList[i].startAngle = currentAngle;

this.data.prizeList[i].endAngle = endAngle;

// 更新 currentAngle,准备绘制下一个区域

currentAngle = endAngle;

}

// 将canvas转换为DataURL格式的图片var dataURL = canvas.toDataURL('image/png', 1);

this.setData({wheelImg: dataURL});

},

startRotate() {

if (this.data.isRotating) return;

this.setData({ isRotating: true });

// 生成随机旋转圈数(至少旋转 6 圈)const randomRounds = 6 + Math.floor(Math.random() * 3);

// 生成随机停止角度const finalAngle = Math.floor(Math.random() * 360);

// 计算总旋转角度const totalRotation = randomRounds * 360 + finalAngle;

// 使用 setInterval 实现动画const startTime = Date.now();

const frameRate = 80;

const rotateAnimation = () => {

const currentTime = Date.now();

const elapsed = currentTime - startTime;

const progress = Math.min(elapsed / this.data.animationDuration, 1);

// 使用 ease-out 动画曲线const easeOut = (t) => 1 - Math.pow(1 - t, 3);

// 计算当前角度const currentAngle = easeOut(progress) * totalRotation;

this.setData({ pointerAngle: currentAngle });

if (progress < 1) {

setTimeout(rotateAnimation, 1000 / frameRate);

} else {

this.stopRotate();

}

};

rotateAnimation();

},

stopRotate() {

let winningIndex = null;

const prizeCount = this.data.prizeList.length;

const anglePerItem = 360 / prizeCount;

const finalAngle = this.data.pointerAngle;

const pointerAngle = (finalAngle) % 360;

// 循环遍历每个奖项区域,判断指针是否落在该区域内for (let i = 0; i < prizeCount; i++) {

const { startAngle, endAngle } = this.data.prizeList[i];

// 处理跨越 0 度的情况,跨越0度就是该区域的开始角度大于结束角度,如340-20if (startAngle > endAngle) {

if (pointerAngle >= startAngle || pointerAngle < endAngle) {

winningIndex = i;

break;

}

} else {

if (pointerAngle >= startAngle && pointerAngle < endAngle) {

winningIndex = i;

break;

}

}

}

this.setData({

isRotating: false

});

const winningPrize = this.data.prizeList[winningIndex].name;

wx.showModal({

title: "恭喜!",

content: `您获得了${winningPrize}!`,

showCancel: false,

});

},

editPrize() {

this.setData({

showModal: true

});

},

closeModal() {

this.setData({

showModal: false

});

},

addNewPrize() {

const colorsLength = this.data.colorsList.length;

const random = Math.floor(Math.random() * colorsLength);

this.setData({

prizeList: [...this.data.prizeList, {

name: '',

color: this.data.colorsList[random] || '#000000'

}],

});

},

deletePrizeItem(e) {

const index = e.currentTarget.dataset.index;

let updatedPrizeList = [...this.data.prizeList];

updatedPrizeList.splice(index, 1); // 从数组中移除对应奖项this.setData({

prizeList: updatedPrizeList,

});

},

updatePrizeName(e) {

const index = e.currentTarget.dataset.index;

const value = e.detail.value;

let updatedPrizeList = this.data.prizeList;

updatedPrizeList[index].name = value;

this.setData({

prizeList: updatedPrizeList,

});

},

shufflePrizeList() {

let updatedPrizeList = [...this.data.prizeList];

for (let i = updatedPrizeList.length - 1; i > 0; i--) {

const j = Math.floor(Math.random() * (i + 1));

[updatedPrizeList[i], updatedPrizeList[j]] = [updatedPrizeList[j], updatedPrizeList[i]];

}

this.setData({

prizeList: updatedPrizeList,

});

},

updatePrizeColor(e) {

const index = e.currentTarget.dataset.index;

const color = e.detail.value;

let updatedPrizeList = this.data.prizeList;

updatedPrizeList[index].color = color;

this.setData({

prizeList: updatedPrizeList,

});

},

savePrizeList() {

// 将 prizeList 转换为 JSON 字符串并存储// 这里可以根据你的需求修改存储方式const prizeListStr = JSON.stringify(this.data.prizeList);

// console.log("保存的 JSON 字符串:", prizeListStr);this.shufflePrizeList(); //打乱奖项的排序,实现随机性this.closeModal();

this.drawPrizeWheel(); // 重新绘制转盘

},

})

index.wxss

.container {

display: flex;

flex-direction: column;

align-items: center;

justify-content: center;

height: auto;

padding-bottom: 0;

padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/

}

.wheel-container {

margin-top: 50rpx;

width: 300px;

height: 300px;

position: relative;

}

.prize-wheel {

width: 100%;

height: 100%;

}

.canvas {

width: 100%;

height: 100%;

}

.pointer {

width: 0px;

height: 0px;

border-bottom: 50px solid #ff0000;

border-left: 10px solid transparent;

border-right: 10px solid transparent;

/* */position: absolute;

bottom: 50%;

left: 50%;

transform-origin: 50% 100%; /* 设置旋转中心点为指针底部 */z-index: 10;

margin-left: -10px; /* 添加 margin-left */margin-top: -10px;

}

.pointer::before{

content: "";

width: 20px;

height: 20px;

border: 0;

padding: 0;

border-radius: 50%;

background-color: #ff0000;

position: absolute;

bottom: -60px;

left: -10px;

z-index: 10;

}

.btn {

margin: 10px;

padding: 10px 20px;

border: none;

border-radius: 5px;

background-color: #007bff;

color: #fff;

font-size: 16px;

}

.edit-btn {

background-color: #28a745;

}

.save-btn {

background-color: #28a745;

margin-top: 20px;

}

.add-btn {

background-color: #1989fa;

margin-top: 10px;

}

.color-preview {

width: 50px;

height: 20px;

border: 1px solid #ccc;

}

.edit-area {

padding: 10rpx;

overflow: auto;

height: auto;

max-height: 620rpx;

}

.delete-btn {

background-color: #dc3545;

/* 红色背景 */color: #fff;

/* 白色文字 */border: none;

border-radius: 50%;

/* 圆形按钮 */display: flex;

align-items: center;

justify-content: center;

width: 55rpx !important;

height: 55rpx !important;

padding: 0;

line-height: 55rpx;

text-align: center;

font-size: 12px;

margin-left: 5px;

}

.prize-item {

display: flex;

align-items: center;

margin-bottom: 10px;

}

.prize-name,

.color-picker {

flex: 1;

padding: 5px;

border: 1px solid #ccc;

margin-right: 5px;

}

注意: 以上代码只是一个示例,你需要根据实际需求进行调整和完善,例如:

添加动画效果,例如转盘加速、减速。

处理用户交互,例如点击开始按钮后禁用按钮,防止重复点击。

优化代码结构,提高代码可读性和可维护性。


另外,我们可以添加一个方法来打乱prizeList数组的顺序,从而增加抽奖的随机性。

在page对象中添加以下方法:

// ... 其他代码 ...

shufflePrizeList() {

let updatedPrizeList = [...this.data.prizeList];

for (let i = updatedPrizeList.length - 1; i > 0; i--) {

const j = Math.floor(Math.random() * (i + 1));

[updatedPrizeList[i], updatedPrizeList[j]] = [updatedPrizeList[j], updatedPrizeList[i]];

}

this.setData({

prizeList: updatedPrizeList,

});

},

// ... 其他代码 ...

方法说明:

shufflePrizeList方法使用 Fisher-Yates shuffle 算法来随机打乱数组元素的顺序。该算法会遍历数组,并将当前元素与随机选取的另一个元素交换位置。

调用该方法:

你可以在以下几种情况下调用shufflePrizeList方法:

在开始旋转转盘之前调用: 这样每次点击 "开始" 按钮时,奖项的顺序都会被打乱,增加随机性。

startRotate() {

if (this.data.isRotating) return;

this.shufflePrizeList(); // 打乱奖项顺序// ... 其他代码 ...

},

在保存奖项列表之后调用: 这样每次编辑完奖项并保存后,奖项的顺序也会被打乱。

savePrizeList() {// ... 保存奖项列表逻辑 ...

this.shufflePrizeList();

// 打乱奖项顺序

this.closeModal();this.drawPrizeWheel();},

在其他合适的时机调用: 根据你的需求,你也可以在其他时机调用该方法,例如在页面加载完成后调用。

选择一个合适的时机调用shufflePrizeList方法,就可以实现打乱奖项顺序,提高抽奖的概率性了。

通过以上修改,现在转盘将不再旋转,而是通过旋转指针来指向最终中奖区域。

希望以上信息能够帮助你! 😊这个功能可以分几步实现:

1. 界面设计:

转盘区域: 使用 canvas 绘制转盘,可配置扇形数量、颜色、文字等。

按钮: "开始/停止" 按钮控制转盘转动。

编辑按钮: 点击弹出弹窗,编辑转盘项目。

中奖弹窗: 显示中奖结果。

2. 数据结构:

使用数组存储转盘项目数据,例如:

const prizeList = [

{ name: '一等奖', color: '#FFD700' },

{ name: '二等奖', color: '#C0C0C0' },

{ name: '三等奖', color: '#CD7F32' },

// ... 其他奖项

];

3. 功能实现:

绘制转盘:

根据 prizeList 数据计算每个扇形的角度。

使用 canvas API 绘制扇形、文字、边框等。

drawPrizeWheel() {

// 创建离屏 2D canvas 实例,创建canvas元素

const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 300});

// 获取 context。注意这里必须要与创建时的 type 一致const ctx = canvas.getContext('2d');

const centerX = 150;

const centerY = 150;

const radius = 130;

const prizeCount = this.data.prizeList.length;

const anglePerItem = 360 / prizeCount;

// 记录当前角度,初始为 0/90 度

let currentAngle = 90;

for (let i = 0; i < prizeCount; i++) {

ctx.beginPath();

ctx.moveTo(centerX, centerY);

ctx.arc(

centerX,

centerY,

radius,

(i * anglePerItem * Math.PI) / 180,

((i + 1) * anglePerItem * Math.PI) / 180,

);

ctx.closePath();

ctx.fillStyle = this.data.prizeList[i].color;

ctx.fill();

// --- 绘制文字 ---

ctx.save();

// 保存当前画布状态

ctx.font = '12px sans-serif';

ctx.fillStyle = '#fff';

ctx.textAlign = 'center';

ctx.textBaseline = 'middle';

const textAngle = (i * anglePerItem + anglePerItem / 2) * (Math.PI) / 180;

const textX = centerX + radius * 0.7 * Math.cos(textAngle);

const textY = centerY + radius * 0.7 * Math.sin(textAngle);

ctx.fillText(this.data.prizeList[i].name, textX, textY);

ctx.restore();

// 恢复之前的画布状态// --- 文字绘制结束 ---

// 计算当前奖项区域的结束角度

let endAngle = currentAngle + anglePerItem;

if(endAngle > 360){

endAngle %= 360;

}

// 计算当前奖项区域的起始角度和结束角度

// 将角度信息存储到 prizeList 数组中

this.data.prizeList[i].startAngle = currentAngle;

this.data.prizeList[i].endAngle = endAngle;

// 更新 currentAngle,准备绘制下一个区域

currentAngle = endAngle;

}

// 将canvas转换为DataURL格式的图片var dataURL = canvas.toDataURL('image/png', 1);this.setData({wheelImg: dataURL});

},

代码解释:

初始化currentAngle: 在循环开始之前,将currentAngle初始化为 90,表示从 90 度开始绘制。因为canvas 绘制圆弧的起始角度是水平向右的 x 轴正方向,而不是竖直向上的 y 轴正方向。所以这里实际起始角度是从90度开始。

计算结束角度: 在每次循环中,根据currentAngle和anglePerItem计算当前奖项区域的结束角度endAngle。

绘制扇形: 使用currentAngle和endAngle绘制当前奖项区域的扇形。最后把canvas转换为base64图片地址。

记录角度信息: 将currentAngle和endAngle存储到prizeList数组中,方便后续判断中奖区域。

更新currentAngle: 将currentAngle更新为endAngle,以便绘制下一个奖项区域。

记录角度信息: 在drawPrizeWheel方法中,我们为每个奖项对象添加了startAngle和endAngle属性,用来存储该奖项区域的起始和结束角度。

判断指针位置: 在stopRotate方法中,我们循环遍历prizeList数组,并根据每个奖项的startAngle和endAngle判断指针是否落在该区域内。

注意处理了跨越 0 度的情况,如果startAngle大于endAngle,则表示该区域跨越了 0 度,需要分别判断指针是否大于等于startAngle或者小于endAngle。

通过以上修改,奖项区域将从 0 度开始绘制,并且每个区域的角度信息会被正确记录在prizeList数组中,然后在stopRotate方法中准确判断指针落在哪个区域,从而确定中奖结果。

转盘转动:

使用setInterval/setTimeout或requestAnimationFrame实现动画效果。

控制转速和停止位置。

<view class="pointer" style="transform: rotate({{pointerAngle}}deg)"></view>

//开始旋转

startRotate() {

if (this.data.isRotating) return;

this.setData({ isRotating: true });

// 生成随机旋转圈数(至少旋转 6 圈)

const randomRounds = 6 + Math.floor(Math.random() * 3);

// 生成随机停止角度

const finalAngle = Math.floor(Math.random() * 360);

// 计算总旋转角度

const totalRotation = randomRounds * 360 + finalAngle;

// 使用 setInterval 实现动画

const startTime = Date.now();const frameRate = 80;

const rotateAnimation = () => {

const currentTime = Date.now();

const elapsed = currentTime - startTime;

const progress = Math.min(elapsed / this.data.animationDuration, 1);

// 使用 ease-out 动画曲线

const easeOut = (t) => 1 - Math.pow(1 - t, 3);

// 计算当前角度

const currentAngle = easeOut(progress) * totalRotation;

this.setData({ pointerAngle: currentAngle });

if (progress < 1) {

setTimeout(rotateAnimation, 1000 / frameRate);

} else {

this.stopRotate();

}

};

rotateAnimation();

},

代码解释:

随机旋转圈数: 在startRotate方法中,我们使用randomRounds变量来生成一个随机的旋转圈数,至少 3 圈,最多 6 圈。

随机停止角度: 使用finalAngle变量生成一个 0 到 360 之间的随机角度,表示指针最终停止的位置。

计算总旋转角度: 将旋转圈数转换为角度,再加上最终停止角度,得到总旋转角度totalRotation。

使用setInterval/setTimeout实现动画: 使用setInterval/setTimeout按照帧率更新指针角度,并使用easeOut动画曲线使指针旋转更自然。

停止动画和判断中奖区域: 在stopRotate方法中,根据最终指针角度finalAngle计算中奖区域索引,并弹出中奖信息。

旋转指针: wxml 中使用style="transform: rotate({{pointerAngle}}deg)"控制指针旋转,pointerAngle存储指针旋转角度。

指针旋转中心: wxss 中设置.pointer的transform-origin: 50% 100%;将旋转中心点设置为指针底部。

stopRotate方法: 修改逻辑,计算指针需要旋转到的角度targetAngle,并更新pointerAngle数据。

现在,转盘指针将会随机旋转几圈,然后随机停止在某个角度,并显示指针指向的奖项作为中奖结果。

中奖判断:

根据停止位置计算中奖索引。

显示中奖弹窗,展示 prizeList 中对应的数据。

stopRotate() {

let winningIndex = null;

const prizeCount = this.data.prizeList.length;

const anglePerItem = 360 / prizeCount;

const finalAngle = this.data.pointerAngle;

const pointerAngle = (finalAngle) % 360;

// 循环遍历每个奖项区域,判断指针是否落在该区域内

for (let i = 0; i < prizeCount; i++) {

const { startAngle, endAngle } = this.data.prizeList[i];

// 处理跨越 0 度的情况,跨越0度就是该区域的开始角度大于结束角度,如340-20

if (startAngle > endAngle) {

if (pointerAngle >= startAngle || pointerAngle < endAngle) {

winningIndex = i;

break;

}

} else {

if (pointerAngle >= startAngle && pointerAngle < endAngle) {

winningIndex = i;

break;

}

}

}

this.setData({

isRotating: false

});

const winningPrize = this.data.prizeList[winningIndex].name;

wx.showModal({

title: "恭喜!",

content: `您获得了${winningPrize}!`,

showCancel: false,

});

},

代码解释:

获取旋转后的角度: finalAngle 表示指针旋转后的随机角度

计算中奖索引:将 finalAngle取余 360,得到相对于第一个奖项区域起始位置的角度pointerAngle 。

遍历奖项区域: 循环遍历每个奖项,计算每个奖项区域的起始角度startAngle和结束角度endAngle。

判断指针位置: 判断pointerAngle是否落在当前奖项区域内 (startAngle到endAngle之间)。

确定中奖索引: 如果指针落在某个奖项区域内,记录下该奖项的索引winningIndex,并跳出循环。

处理结果: 根据winningIndex获取中奖信息,并进行后续处理。如果winningIndex为null,则表示出现异常,需要进行相应的处理。

注意:

(startAngle到endAngle之间)存在跨越0度的情况,要进行判断

如果你的奖项区域绘制顺序或方向与示例不同,你需要相应地调整计算逻辑。

希望这个解决方案可以帮助你!

编辑转盘:

弹窗中使用列表展示 prizeList 数据,可以进行增删改操作。

修改 prizeList 数据后,重新绘制转盘。

editPrize() {

this.setData({

showModal: true

});

},

closeModal() {

this.setData({

showModal: false

});

},

addNewPrize() {

const colorsLength = this.data.colorsList.length;

const random = Math.floor(Math.random() * colorsLength);

this.setData({

prizeList: [...this.data.prizeList, {

name: '',

color: this.data.colorsList[random] || '#000000'

}],

});

},

deletePrizeItem(e) {

const index = e.currentTarget.dataset.index;

let updatedPrizeList = [...this.data.prizeList];

updatedPrizeList.splice(index, 1); // 从数组中移除对应奖项this.setData({

prizeList: updatedPrizeList,

});

},

updatePrizeName(e) {

const index = e.currentTarget.dataset.index;

const value = e.detail.value;

let updatedPrizeList = this.data.prizeList;

updatedPrizeList[index].name = value;

this.setData({

prizeList: updatedPrizeList,

});

},

updatePrizeColor(e) {

const index = e.currentTarget.dataset.index;

const color = e.detail.value;

let updatedPrizeList = this.data.prizeList;

updatedPrizeList[index].color = color;

this.setData({

prizeList: updatedPrizeList,

});

},

savePrizeList() {

// 将 prizeList 转换为 JSON 字符串并存储// 这里可以根据你的需求修改存储方式const prizeListStr = JSON.stringify(this.data.prizeList);

// console.log("保存的 JSON 字符串:", prizeListStr);this.closeModal();

this.drawPrizeWheel(); // 重新绘制转盘

},

4. 代码示例:

index.wxml

<view class="container"><view class="wheel-container">

<view class="prize-wheel">

<image class="canvas" src="{{wheelImg}}"></image></view><view class="pointer" style="transform: rotate({{pointerAngle}}deg)"></view>

</view><button class="btn" bindtap="startRotate" disabled="{{isRotating}}">开始</button><button class="btn edit-btn" bindtap="editPrize">编辑</button>

<modal title="编辑奖项" hidden="{{!showModal}}" bindcancel="closeModal">

<view class="edit-area">

<!-- <view class="color-picker-slider">

<view>R: <slider min="0" max="255" value="{{r}}" bindchange="onColorSliderChange" data-channel="r"/></view>

<view>G: <slider min="0" max="255" value="{{g}}" bindchange="onColorSliderChange" data-channel="g"/></view>

<view>B: <slider min="0" max="255" value="{{b}}" bindchange="onColorSliderChange" data-channel="b"/></view>

<view class="color-preview" style=" color: rgb(189, 99, 197);">{{r}}, {{g}}, {{b}});"></view>

</view> -->

<view class="prize-item" wx:for="{{prizeList}}" wx:key="index"><input

class="prize-name"

placeholder="奖项名称"

value="{{item.name}}"

data-index="{{index}}"bindinput="updatePrizeName"

/><input

class="color-picker"

type="color"

value="{{item.color}}"

data-index="{{index}}"bindchange="updatePrizeColor"

/>

<button class="delete-btn" data-index="{{index}}" bindtap="deletePrizeItem">-</button>

</view>

</view>

<button class="btn add-btn" bindtap="addNewPrize">+</button><button class="btn save-btn" bindtap="savePrizeList">保存</button>

</modal>

</view>

index.js

Page({

data: {

prizeList: [{

name: '一等奖',

color: '#FFD700'

},

{

name: '谢谢参与',

color: '#C0C0C0'

},

{

name: '二等奖',

color: '#CD7F32'

},

{

name: '谢谢参与',

color: '#C0C0C0'

},

{

name: '三等奖',

color: '#A0522D'

},

{

name: '谢谢参与',

color: '#C0C0C0'

},

],

prizeListStr: '',

showModal: false,

rotateAngle: 0, // 用于模拟转盘动画,实际并未使用animation: null, // 未使用pointerAngle: 0, // 指针角度isRotating: false, // 是否正在旋转animationDuration: 5000, // 动画持续时间 (毫秒)showColorPickerVisible: false,

currentPrizeIndex: null,

r: 0,

g: 0,

b: 0,

colorsList: [

'#FFD700', '#C0C0C0', '#CD7F32', '#A0522D', '#DC143C', '#FF69B4',

'#BA55D3', '#7B68EE', '#6A5ACD', '#483D8B', '#4682B4', '#00CED1',

'#5F9EA0', '#2E8B57', '#9ACD32', '#FFFF00', '#FFA500', '#FF4500',

'#8B4513', '#D2691E', '#B8860B', '#808000', '#556B2F', '#228B22',

'#008000', '#006400', '#90EE90', '#00FF7F', '#00FA9A', '#20B2AA',

'#235788', '#697656', '#D935E9', '#B69754', '#BFA476', '#BAC289',

'#983471', '#EA4586', '#EABCDE', '#ACBEAD', '#825673', '#65C946',

],

},

onLoad() {

this.drawPrizeWheel();

},

onReady() {

},

onColorSliderChange(e) {

const channel = e.currentTarget.dataset.channel;

const value = e.detail.value;

this.setData({

[channel]: value

});

},

drawPrizeWheel() {

// 创建离屏 2D canvas 实例,创建canvas元素const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 300});

// 获取 context。注意这里必须要与创建时的 type 一致const ctx = canvas.getContext('2d');

const centerX = 150;

const centerY = 150;

const radius = 130;

const prizeCount = this.data.prizeList.length;

const anglePerItem = 360 / prizeCount;

// 记录当前角度,初始为 0/90 度let currentAngle = 90;

for (let i = 0; i < prizeCount; i++) {

ctx.beginPath();

ctx.moveTo(centerX, centerY);

ctx.arc(

centerX,

centerY,

radius,

(i * anglePerItem * Math.PI) / 180,

((i + 1) * anglePerItem * Math.PI) / 180,

);

ctx.closePath();

ctx.fillStyle = this.data.prizeList[i].color;

ctx.fill();

// --- 绘制文字 ---

ctx.save(); // 保存当前画布状态

ctx.font = '12px sans-serif';

ctx.fillStyle = '#fff';

ctx.textAlign = 'center';

ctx.textBaseline = 'middle';

const textAngle = (i * anglePerItem + anglePerItem / 2) * (Math.PI) / 180;

const textX = centerX + radius * 0.7 * Math.cos(textAngle);

const textY = centerY + radius * 0.7 * Math.sin(textAngle);

ctx.fillText(this.data.prizeList[i].name, textX, textY);

ctx.restore(); // 恢复之前的画布状态// --- 文字绘制结束 ---

// 计算当前奖项区域的结束角度let endAngle = currentAngle + anglePerItem;

if(endAngle > 360){

endAngle %= 360;

}

// 计算当前奖项区域的起始角度和结束角度// 将角度信息存储到 prizeList 数组中this.data.prizeList[i].startAngle = currentAngle;

this.data.prizeList[i].endAngle = endAngle;

// 更新 currentAngle,准备绘制下一个区域

currentAngle = endAngle;

}

// 将canvas转换为DataURL格式的图片var dataURL = canvas.toDataURL('image/png', 1);

this.setData({wheelImg: dataURL});

},

startRotate() {

if (this.data.isRotating) return;

this.setData({ isRotating: true });

// 生成随机旋转圈数(至少旋转 6 圈)const randomRounds = 6 + Math.floor(Math.random() * 3);

// 生成随机停止角度const finalAngle = Math.floor(Math.random() * 360);

// 计算总旋转角度const totalRotation = randomRounds * 360 + finalAngle;

// 使用 setInterval 实现动画const startTime = Date.now();

const frameRate = 80;

const rotateAnimation = () => {

const currentTime = Date.now();

const elapsed = currentTime - startTime;

const progress = Math.min(elapsed / this.data.animationDuration, 1);

// 使用 ease-out 动画曲线const easeOut = (t) => 1 - Math.pow(1 - t, 3);

// 计算当前角度const currentAngle = easeOut(progress) * totalRotation;

this.setData({ pointerAngle: currentAngle });

if (progress < 1) {

setTimeout(rotateAnimation, 1000 / frameRate);

} else {

this.stopRotate();

}

};

rotateAnimation();

},

stopRotate() {

let winningIndex = null;

const prizeCount = this.data.prizeList.length;

const anglePerItem = 360 / prizeCount;

const finalAngle = this.data.pointerAngle;

const pointerAngle = (finalAngle) % 360;

// 循环遍历每个奖项区域,判断指针是否落在该区域内for (let i = 0; i < prizeCount; i++) {

const { startAngle, endAngle } = this.data.prizeList[i];

// 处理跨越 0 度的情况,跨越0度就是该区域的开始角度大于结束角度,如340-20if (startAngle > endAngle) {

if (pointerAngle >= startAngle || pointerAngle < endAngle) {

winningIndex = i;

break;

}

} else {

if (pointerAngle >= startAngle && pointerAngle < endAngle) {

winningIndex = i;

break;

}

}

}

this.setData({

isRotating: false

});

const winningPrize = this.data.prizeList[winningIndex].name;

wx.showModal({

title: "恭喜!",

content: `您获得了${winningPrize}!`,

showCancel: false,

});

},

editPrize() {

this.setData({

showModal: true

});

},

closeModal() {

this.setData({

showModal: false

});

},

addNewPrize() {

const colorsLength = this.data.colorsList.length;

const random = Math.floor(Math.random() * colorsLength);

this.setData({

prizeList: [...this.data.prizeList, {

name: '',

color: this.data.colorsList[random] || '#000000'

}],

});

},

deletePrizeItem(e) {

const index = e.currentTarget.dataset.index;

let updatedPrizeList = [...this.data.prizeList];

updatedPrizeList.splice(index, 1); // 从数组中移除对应奖项this.setData({

prizeList: updatedPrizeList,

});

},

updatePrizeName(e) {

const index = e.currentTarget.dataset.index;

const value = e.detail.value;

let updatedPrizeList = this.data.prizeList;

updatedPrizeList[index].name = value;

this.setData({

prizeList: updatedPrizeList,

});

},

shufflePrizeList() {

let updatedPrizeList = [...this.data.prizeList];

for (let i = updatedPrizeList.length - 1; i > 0; i--) {

const j = Math.floor(Math.random() * (i + 1));

[updatedPrizeList[i], updatedPrizeList[j]] = [updatedPrizeList[j], updatedPrizeList[i]];

}

this.setData({

prizeList: updatedPrizeList,

});

},

updatePrizeColor(e) {

const index = e.currentTarget.dataset.index;

const color = e.detail.value;

let updatedPrizeList = this.data.prizeList;

updatedPrizeList[index].color = color;

this.setData({

prizeList: updatedPrizeList,

});

},

savePrizeList() {

// 将 prizeList 转换为 JSON 字符串并存储// 这里可以根据你的需求修改存储方式const prizeListStr = JSON.stringify(this.data.prizeList);

// console.log("保存的 JSON 字符串:", prizeListStr);this.shufflePrizeList(); //打乱奖项的排序,实现随机性this.closeModal();

this.drawPrizeWheel(); // 重新绘制转盘

},

})

index.wxss

.container {

display: flex;

flex-direction: column;

align-items: center;

justify-content: center;

height: auto;

padding-bottom: 0;

padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/

}

.wheel-container {

margin-top: 50rpx;

width: 300px;

height: 300px;

position: relative;

}

.prize-wheel {

width: 100%;

height: 100%;

}

.canvas {

width: 100%;

height: 100%;

}

.pointer {

width: 0px;

height: 0px;

border-bottom: 50px solid #ff0000;

border-left: 10px solid transparent;

border-right: 10px solid transparent;

/* */position: absolute;

bottom: 50%;

left: 50%;

transform-origin: 50% 100%; /* 设置旋转中心点为指针底部 */z-index: 10;

margin-left: -10px; /* 添加 margin-left */margin-top: -10px;

}

.pointer::before{

content: "";

width: 20px;

height: 20px;

border: 0;

padding: 0;

border-radius: 50%;

background-color: #ff0000;

position: absolute;

bottom: -60px;

left: -10px;

z-index: 10;

}

.btn {

margin: 10px;

padding: 10px 20px;

border: none;

border-radius: 5px;

background-color: #007bff;

color: #fff;

font-size: 16px;

}

.edit-btn {

background-color: #28a745;

}

.save-btn {

background-color: #28a745;

margin-top: 20px;

}

.add-btn {

background-color: #1989fa;

margin-top: 10px;

}

.color-preview {

width: 50px;

height: 20px;

border: 1px solid #ccc;

}

.edit-area {

padding: 10rpx;

overflow: auto;

height: auto;

max-height: 620rpx;

}

.delete-btn {

background-color: #dc3545;

/* 红色背景 */color: #fff;

/* 白色文字 */border: none;

border-radius: 50%;

/* 圆形按钮 */display: flex;

align-items: center;

justify-content: center;

width: 55rpx !important;

height: 55rpx !important;

padding: 0;

line-height: 55rpx;

text-align: center;

font-size: 12px;

margin-left: 5px;

}

.prize-item {

display: flex;

align-items: center;

margin-bottom: 10px;

}

.prize-name,

.color-picker {

flex: 1;

padding: 5px;

border: 1px solid #ccc;

margin-right: 5px;

}

注意: 以上代码只是一个示例,你需要根据实际需求进行调整和完善,例如:

添加动画效果,例如转盘加速、减速。

处理用户交互,例如点击开始按钮后禁用按钮,防止重复点击。

优化代码结构,提高代码可读性和可维护性。


另外,我们可以添加一个方法来打乱prizeList数组的顺序,从而增加抽奖的随机性。

在page对象中添加以下方法:

// ... 其他代码 ...

shufflePrizeList() {

let updatedPrizeList = [...this.data.prizeList];

for (let i = updatedPrizeList.length - 1; i > 0; i--) {

const j = Math.floor(Math.random() * (i + 1));

[updatedPrizeList[i], updatedPrizeList[j]] = [updatedPrizeList[j], updatedPrizeList[i]];

}

this.setData({

prizeList: updatedPrizeList,

});

},

// ... 其他代码 ...

方法说明:

shufflePrizeList方法使用 Fisher-Yates shuffle 算法来随机打乱数组元素的顺序。该算法会遍历数组,并将当前元素与随机选取的另一个元素交换位置。

调用该方法:

你可以在以下几种情况下调用shufflePrizeList方法:

在开始旋转转盘之前调用: 这样每次点击 "开始" 按钮时,奖项的顺序都会被打乱,增加随机性。

startRotate() {

if (this.data.isRotating) return;

this.shufflePrizeList(); // 打乱奖项顺序// ... 其他代码 ...

},

在保存奖项列表之后调用: 这样每次编辑完奖项并保存后,奖项的顺序也会被打乱。

savePrizeList() {// ... 保存奖项列表逻辑 ...

this.shufflePrizeList();

// 打乱奖项顺序

this.closeModal();this.drawPrizeWheel();},

在其他合适的时机调用: 根据你的需求,你也可以在其他时机调用该方法,例如在页面加载完成后调用。

选择一个合适的时机调用shufflePrizeList方法,就可以实现打乱奖项顺序,提高抽奖的概率性了。

通过以上修改,现在转盘将不再旋转,而是通过旋转指针来指向最终中奖区域。

希望以上信息能够帮助你! 😊

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容