Uni-APP 组件-抽奖轮盘
组件预览
创建commponents/lottery/index.vue 文件
输入如下代码
轮盘抽检组件
<template>
<view class="main">
<view v-if="options.length > 0" class="canvas-container">
<canvas canvas-id="canvas" id="canvas" :style="canvasStyle" />
<canvas canvas-id="canvasBtn" id="canvasBtn" class="canvasBtn" :style="canvasStyle" />
<cover-view @tap="playReward" :class="['canvas-btn', { 'isLottery': isLottery }]"
:style="{ backgroundColor: centerBtnColor }">
{{ centerBtnText }}
</cover-view>
</view>
<view v-else class="empty-tip">
<text>暂无抽奖选项</text>
</view>
<view v-if="showBtn" class="btn-container">
<button type="primary" :disabled="isLottery" @click="playReward">{{ btnTitle }}</button>
</view>
</view>
</template>
<script>
var ctx = null;
export default {
props: {
// 弹窗内容 ,当 turnModalContent=【】时,默认弹窗内容为抽奖结果,并且不展示标题
turnModalContent: {
type: Array,
default: () => []
},
// 减速,值越小,减速效果越明显 turnReduceSpeed
turnReduceSpeed: {
type: Number,
default: 50
},
// 表示转速:表示再转多少圈再进行抽奖
turnCircle: {
type: Number,
default: 0
},
// 画布宽度
width: {
type: Number,
default: 100,
},
// 画布高度
height: {
type: Number,
default: 100
},
// 画布内字体大小
fontSize: {
type: Number,
default: 18
},
// 钩子函数,抽奖开始前执行,返回值为选项列表下标,若返回值为-1,则进行随机抽奖
setWinnerFn: {
type: Function,
default: null,
},
// // 钩子函数,抽奖开始前执行
beforePlay: {
type: Function,
default: null,
},
// // 钩子函数,抽奖结束后执行
afterPlay: {
type: Function,
default: null,
},
// 如果 true,则 使用组件默认的选项数据
isUseDefaultOptions: {
type: Boolean,
default: false
},
// 选项列表
data: {
type: Array,
default: () => []
},
// 是否要展示开始按钮
showBtn: {
type: Boolean,
default: false,
},
// 按钮文本
btnTitle: {
type: String,
default: "开始"
},
// 是否显示结果弹窗
showResultModal: {
type: Boolean,
default: true
},
// 中心按钮文本
centerBtnText: {
type: String,
default: "GO"
},
// 中心按钮颜色
centerBtnColor: {
type: String,
default: "#ffffff"
}
},
data() {
return {
options: [{
id: 1, // 唯一id
name: '水饺', // 名称
// "weight": 50, // 中奖权重,0-100
img: '', // 展示图片
color: "#f6e174" // 轮盘区域底色
},
{
id: 2,
name: '火锅',
img: '',
color: "#94494d"
},
{
id: 3,
name: '川菜',
img: '',
color: "#ffaa7f"
},
{
id: 4,
name: '麻辣烫',
img: '',
color: "#a48342"
},
{
id: 5,
name: '炸鸡汉堡',
img: '',
color: "#a25f81"
},
{
id: 6,
name: '重庆小面',
img: '',
color: "#98e2e2"
},
{
id: 7,
name: '酸辣粉',
img: '',
color: "#aaaa00"
},
{
id: 8,
name: '面条',
img: '',
color: "#648f76"
},
{
id: 9,
name: '汤河粉',
img: '',
color: "#40b3ff"
},
{
id: 10,
name: '猪脚饭',
img: '',
color: "#ffaaff"
},
{
id: 11,
name: '烧鸭饭',
img: '',
color: "#5555ff"
},
{
id: 12,
name: '白切鸡',
img: '',
color: "#ff90cf"
},
{
id: 13,
name: '烧鸡饭',
img: '',
color: "#55aa7f"
},
{
id: 14,
name: '肥肠面',
img: '',
color: "#4f95ff"
},
{
id: 15,
name: '炒饭',
img: '',
color: "#a509aa"
},
{
id: 16,
name: '黄焖鸡米饭',
img: '',
color: "#aaaa00"
},
{
id: 17,
name: '皮蛋瘦肉粥',
img: '',
color: "#ff52d1"
},
{
id: 18,
name: '沙县小吃',
img: '',
color: "#944ade"
},
{
id: 19,
name: '焖锅',
img: '',
color: "#43c900"
},
{
id: 20,
name: '披萨',
img: '',
color: "#7cbaba"
}
],
isLottery: false, // 是否正在抽奖
winnerIndex: -1, // 中奖索引
isInitialized: false // 是否已初始化
};
},
computed: {
canvasStyle() {
return "width:" + this.width + "px;; height:" + this.height + "px;";
},
},
watch: {
// 监听数据变化,重新初始化
data: {
handler(newVal) {
if (newVal && newVal.length > 0 && !this.isUseDefaultOptions) {
this.init();
}
},
deep: true
}
},
methods: {
// 处理文本,根据长度进行截断或调整
processText(text, maxLength) {
if (!text) return '';
// 如果文本长度超过最大长度,截断并添加省略号
if (text.length > maxLength) {
return text.substring(0, maxLength - 2) + '..';
}
return text;
},
// 获取文本显示的字体大小
getTextFontSize(text, baseSize) {
// 根据文本长度动态调整字体大小
if (!text) return baseSize;
if (text.length > 6) {
// 文本较长时,逐步减小字体
const reduction = Math.min(Math.floor(text.length / 2), 8);
return Math.max(baseSize - reduction, 10); // 最小字体为10
}
return baseSize;
},
async playReward() {
if (this.isLottery) {
return
}
let len = this.options.length
if (len == 0) {
console.error("抽奖选项为空,无法开始抽奖");
return;
}
this.isLottery = true
console.log("开始抽奖")
// 触发beforePlay事件
if (this.beforePlay) {
this.beforePlay();
}
this.$emit("beforePlay");
// 获取中奖索引
let num = -1;
// 使用setWinnerFn获取中奖索引
if (this.setWinnerFn && typeof this.setWinnerFn === 'function') {
try {
// 修复调用方式
num = this.setWinnerFn(this.options);
if (num === undefined || num < 0 || num >= len) {
console.warn("setWinnerFn返回的索引无效,将使用随机抽奖");
num = -1;
}
} catch (error) {
console.error("调用setWinnerFn出错:", error);
num = -1;
}
}
if (num < 0) {
// 进行权重抽奖
let optionIndex = this.lotteryWeight(this.options)
if (optionIndex !== undefined && optionIndex >= 0) {
num = optionIndex
} else {
// 没有自动抽奖结果 && 没有定义选项权重,则进行随机数抽奖
num = Math.floor(Math.random() * len)
}
}
this.winnerIndex = num;
try {
const result = await this.roateCanvas(num)
console.log("抽奖结果:", result.name)
this.isLottery = false
// 是否显示结果弹窗
if (this.showResultModal) {
let title = ""
let content = result.name
if (this.turnModalContent.length > 0) {
// 若设置了 content ,则随机取一个 content 内容,并且 title 将展示为抽奖结果
title = result.name
content = this.turnModalContent[Math.floor(Math.random() * this.turnModalContent.length)]
}
uni.showModal({
confirmText: "确定",
showCancel: false,
title: title,
content: content,
success: (res) => {
if (res.confirm) {
console.log('用户点击确定');
// 调用钩子函数
if (this.afterPlay) {
this.afterPlay(result);
}
this.$emit("afterPlay", result)
}
}
})
} else {
// 直接触发afterPlay事件
if (this.afterPlay) {
this.afterPlay(result);
}
this.$emit("afterPlay", result)
}
} catch (error) {
console.error("抽奖过程出错:", error);
this.isLottery = false;
uni.showToast({
title: '抽奖失败,请重试',
icon: 'none'
});
}
},
initBtnCanvas() {
try {
let angleTo = 0
let ctx = uni.createCanvasContext("canvasBtn", this);
// 圆中心点的坐标 x: 宽度的一半
let center_x = this.width / 2;
// 圆中心点的坐标 y: 高度的一半
let center_y = this.height / 2;
// 先清除画布上在该矩形区域内的内容
ctx.clearRect(0, 0, this.width, this.height);
ctx.translate(center_x, center_y);
// 画中心点圆
ctx.beginPath();
ctx.moveTo(0, -50); // 三角形顶点坐标
ctx.lineTo(-20, 10); // 左下角坐标
ctx.lineTo(20, 10); // 右下角坐标
ctx.setFillStyle("#ff0000");
ctx.fill();
ctx.draw();
} catch (error) {
console.error("初始化按钮画布失败:", error);
}
},
initCanvas: function (ctx, angleTo) {
try {
const len = this.options.length; //数组长度
if (len == 0) {
console.warn("options len == 0");
return;
}
if (!angleTo) {
angleTo = 0
}
// 圆中心点的坐标 x: 宽度的一半
let center_x = this.width / 2;
// 圆中心点的坐标 y: 高度的一半
let center_y = this.height / 2;
// 圆的弧度的总度数,2π表示画圆
let totalAngle = 2 * Math.PI;
// 平均一个选项占用的孤度数
let avgAngle = totalAngle / len;
let radius = center_x - 14;
let baseFontSize = this.getFontSize();
// 先清除画布上在该矩形区域内的内容
ctx.clearRect(0, 0, this.width, this.height);
ctx.translate(center_x, center_y);
ctx.setLineWidth(14);
ctx.save();
// 画外圆
ctx.rotate(angleTo * Math.PI / 180);
var beginAngle = 2 * Math.PI / 360 * (-90);
ctx.setStrokeStyle("#ffffff");
ctx.arc(0, 0, radius - 3, 0, Math.PI * 2);
ctx.stroke();
// 划分区域,并且填充颜色
ctx.setLineWidth(0.1);
beginAngle = 2 * Math.PI / 360 * (-90);
//绘制填充形状
for (var i = 0; i < len; i++) {
ctx.save();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.setStrokeStyle(this.options[i].color);
ctx.setFillStyle(this.options[i].color);
ctx.arc(0, 0, radius, beginAngle, beginAngle + avgAngle, false);
ctx.fill();
ctx.save();
beginAngle = beginAngle + avgAngle;
}
// 绘制选项文字
beginAngle = 0;
for (var i = 0; i < len; i++) {
// 根据奖项数量和文本长度调整文本位置和大小
let textDistance = -(center_x / 2) - 25;
// 奖项过多时,文本更靠近中心
if (len > 10) {
textDistance = -(center_x / 2) - 15;
}
if (len > 15) {
textDistance = -(center_x / 2) - 5;
}
// 处理文本,根据奖项数量决定最大长度
let maxTextLength = len > 12 ? 4 : (len > 8 ? 6 : 8);
let processedText = this.processText(this.options[i].name, maxTextLength);
// 根据文本长度调整字体大小
let fontSize = this.getTextFontSize(processedText, baseFontSize);
//绘制旋转文字
ctx.rotate((beginAngle + (avgAngle * 0.5))); //顺时针旋转
ctx.setTextAlign("center");
ctx.setFillStyle("#FFFFFF");
ctx.setFontSize(fontSize);
ctx.fillText(processedText, 0, textDistance);
ctx.restore();
beginAngle = beginAngle + avgAngle;
}
ctx.save();
// 画中心点圆
ctx.beginPath();
ctx.arc(0, 0, 15, 0, Math.PI * 2); // 15 为中心点圆的半径
ctx.setFillStyle("#FFFFFF");
ctx.fill();
ctx.draw();
this.isInitialized = true;
} catch (error) {
console.error("初始化画布失败:", error);
this.isInitialized = false;
}
},
// 根据设置的权重进行抽奖
lotteryWeight(prizes) {
console.log("进行权重抽奖")
if (!prizes || prizes.length === 0) {
console.log("奖品列表为空")
return -1
}
// 计算总权重和有效奖品
let totalWeight = 0
const validPrizes = []
// 筛选有效奖品并计算总权重
for (let index = 0; index < prizes.length; index++) {
const prize = prizes[index]
const weight = Number(prize.weight)
// 只有权重大于0的奖品才参与抽奖
if (weight && weight > 0) {
totalWeight += weight
validPrizes.push({
index,
weight,
name: prize.name
})
}
}
// 如果没有有效奖品,返回-1表示随机抽奖
if (validPrizes.length === 0 || totalWeight === 0) {
console.log("没有设置有效权重的奖品,将进行随机抽奖")
return -1
}
// 生成0到总权重之间的随机数
const random = Math.random() * totalWeight
console.log("权重抽奖随机数:", random, "总权重:", totalWeight)
// 根据权重区间确定中奖奖品
let accumulatedWeight = 0
for (const prize of validPrizes) {
accumulatedWeight += prize.weight
if (random < accumulatedWeight) {
console.log("权重抽奖奖品 ", prizes[prize.index].name, " 中奖,权重:", prize.weight)
return prize.index
}
}
// 保底返回最后一个有效奖品
console.log("未找到匹配奖品,进行随机抽奖")
return -1
},
// 旋转画布,num 表示选项 options 的下标
roateCanvas(num) {
let len = this.options.length
let angle = 360 / len;
angle = num * angle + angle / 2;
angle = angle || 0;
angle = 360 - angle;
angle += 360 * 5;
if (this.turnCircle > 0) {
let turnCircle = this.turnCircle
// 最多只能转 10 圈
if (turnCircle > 10) {
turnCircle = 10
}
angle += 360 * turnCircle;
}
let that = this;
let count = 1;
// 减速,值越小,减速效果越明显
let turnReduceSpeed = this.turnReduceSpeed
if (turnReduceSpeed == 0) {
turnReduceSpeed = 1
}
let baseStep = turnReduceSpeed;
// 起始滚动速度
let baseSpeed = 1;
let result = {}
return new Promise((resolve, reject) => {
let timer = setInterval(function () {
// console.log("count = ", count);
that.initCanvas(that.ctx, count);
if (count == angle) {
clearInterval(timer);
result = that.options[num];
resolve(result)
}
count = count + baseStep * (((angle - count) / angle) > baseSpeed ? baseSpeed :
((angle -
count) / angle)) + 0.1;
if (angle - count < 0.5) {
count = angle;
}
}, 25);
});
},
getFontSize() {
let fontSize = this.fontSize
if (this.options.length > 10) {
if (this.fontSize >= 18) {
fontSize = this.fontSize - (this.options.length - 10)
}
}
return fontSize
},
// 生成随机颜色
genRandColor() {
// 生成随机的 RGB 值
var r = Math.floor(Math.random() * 256); // 0 到 255 之间的随机数
var g = Math.floor(Math.random() * 256);
var b = Math.floor(Math.random() * 256);
// 将 RGB 值转换为 Hex 颜色表示
var hexColor = "#" + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b);
return hexColor;
},
componentToHex(c) {
var hex = c.toString(16);
return hex.length === 1 ? "0" + hex : hex;
},
initOptions: function () {
try {
let defaultOptions = JSON.parse(JSON.stringify(this.options)); // 深拷贝默认选项
if (this.isUseDefaultOptions) {
// 使用默认数据
console.log("使用默认选项数据");
} else if (this.data && this.data.length > 0) {
console.log("使用传入的选项数据, 长度:", this.data.length);
this.options = JSON.parse(JSON.stringify(this.data)); // 深拷贝传入数据
} else {
console.warn("未提供有效的选项数据,将使用默认选项");
}
// 所有默认的颜色
let allDefColorArr = defaultOptions.map(item => item.color);
// 找到最大的 id
let maxId = 0;
this.options.forEach(item => {
if (item.id && item.id > maxId) {
maxId = item.id;
}
});
// 填充 id,确保 id 唯一
var existIds = []
for (var i = 0; i < this.options.length; i++) {
let item = this.options[i]
if (item['id'] == undefined || item.id == 0 || existIds.includes(item.id)) {
// id 不存在,或者 id 重复了
maxId++;
this.options[i]['id'] = maxId;
}
existIds.push(this.options[i].id)
}
// 填充颜色,确保颜色唯一
let availableColor = JSON.parse(JSON.stringify(allDefColorArr)) // 可用颜色
let existColor = [] // 存在颜色
// 先收集已有颜色
for (var i = 0; i < this.options.length; i++) {
let item = this.options[i]
if (item['color'] && item.color !== "") {
let color = item.color
existColor.push(color)
// 过滤掉已经用了的 color
availableColor = availableColor.filter(c => c !== color);
}
}
// 为没有颜色的选项分配颜色
for (var i = 0; i < this.options.length; i++) {
let item = this.options[i]
if (!item['color'] || item.color === "") {
if (availableColor.length == 0) {
// 没有可用颜色了,则随机生成一个
let color = '';
for (var j = 0; j < 100; j++) {
if (color !== '') break;
let genColor = this.genRandColor()
if (!existColor.includes(genColor)) {
existColor.push(genColor)
color = genColor
}
}
this.options[i]['color'] = color
} else {
const color = availableColor.shift();
existColor.push(color)
this.options[i].color = color
}
}
}
console.log("选项初始化完成,数量:", this.options.length);
} catch (error) {
console.error("初始化选项失败:", error);
// 确保至少有默认选项
if (!this.options || this.options.length === 0) {
this.options = [{
id: 1,
name: '默认选项',
color: "#f6e174"
}];
}
}
},
init() {
try {
this.initOptions();
this.ctx = uni.createCanvasContext("canvas", this);
this.initCanvas(this.ctx, 0);
this.initBtnCanvas();
} catch (error) {
console.error("初始化组件失败:", error);
uni.showToast({
title: '初始化抽奖组件失败',
icon: 'none'
});
}
}
},
// 初始化画布
mounted: function () {
console.log("lottery mounted init......")
this.init()
}
}
</script>
<style scoped>
.main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.title-container {
font-size: 25px;
margin-top: 120rpx;
margin-bottom: 40rpx;
z-index: 100;
}
.canvas-container {
position: relative;
width: fit-content;
height: fit-content;
}
.canvasBtn {
position: absolute;
top: 0%;
}
.canvas-btn {
position: absolute;
top: 50%;
left: 50%;
background-color: #ffffff;
transform: translate(-50%, -50%);
width: 110rpx;
height: 110rpx;
border-radius: 50%;
line-height: 110rpx;
text-align: center;
font-size: 45rpx;
text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.6);
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.6);
text-decoration: none;
}
.canvas-btn.isLottery {
background-color: #CCCCCC;
pointer-events: none;
}
.empty-tip {
padding: 30rpx;
text-align: center;
color: #999;
font-size: 28rpx;
}
.btn-container {
margin-top: 30rpx;
}
</style>
测试Demo
<template>
<view class="container">
<view class="title">转盘抽奖组件演示</view>
<!-- 转盘抽奖组件 -->
<lottery ref="lotteryRef" :width="300" :height="300" :fontSize="16" :data="lotteryOptions"
:isUseDefaultOptions="useDefaultOptions" :turnReduceSpeed="reduceSpeed" :turnCircle="circleCount"
:showBtn="true" :btnTitle="'开始抽奖'" :centerBtnText="'GO'" :centerBtnColor="'#ff5252'"
:showResultModal="showModal" :turnModalContent="modalContents" :setWinnerFn="setWinner"
:beforePlay="onBeforePlay" :afterPlay="onAfterPlay" />
<!-- 配置面板 -->
<view class="config-panel">
<view class="panel-title">配置选项</view>
<view class="config-item">
<text>使用默认选项:</text>
<switch :checked="useDefaultOptions" @change="(e) => useDefaultOptions = e.detail.value" />
</view>
<view class="config-item">
<text>减速效果({{ reduceSpeed }}):</text>
<slider :value="reduceSpeed" :min="1" :max="100" :step="1" show-value
@change="(e) => reduceSpeed = e.detail.value" />
</view>
<view class="config-item">
<text>额外旋转圈数({{ circleCount }}):</text>
<slider :value="circleCount" :min="0" :max="10" :step="1" show-value
@change="(e) => circleCount = e.detail.value" />
</view>
<view class="config-item">
<text>显示结果弹窗:</text>
<switch :checked="showModal" @change="(e) => showModal = e.detail.value" />
</view>
<view class="config-item">
<text>指定中奖项:</text>
<picker :value="winnerIndex" :range="winnerOptions" range-key="name" @change="onWinnerChange">
<view class="picker-value">{{ winnerOptions[winnerIndex].name || '随机' }}</view>
</picker>
</view>
<button class="reset-btn" type="default" @click="resetLottery">重置转盘</button>
</view>
<!-- 事件日志 -->
<view class="event-log">
<view class="log-title">事件日志</view>
<scroll-view scroll-y class="log-content">
<view v-for="(log, index) in eventLogs" :key="index" class="log-item">
{{ log }}
</view>
</scroll-view>
<button size="mini" type="default" @click="clearLogs">清空日志</button>
</view>
</view>
</template>
<script>
import Lottery from '@/components/lottery/index.vue'
export default {
// 注册组件
components: {
Lottery
},
data() {
return {
// 转盘选项数据
lotteryOptions: [
{ id: 1, name: '一等奖', weight: 5, color: '#ff4444' },
{ id: 2, name: '二等奖', weight: 10, color: '#ff8800' },
{ id: 3, name: '三等奖', weight: 15, color: '#ffcc00' },
{ id: 4, name: '四等奖', weight: 20, color: '#33cc33' },
{ id: 5, name: '五等奖', weight: 25, color: '#3399ff' },
{ id: 6, name: '谢谢参与', weight: 25, color: '#9966ff' }
],
// 配置选项
useDefaultOptions: false, // 是否使用组件默认选项
reduceSpeed: 50, // 减速效果,值越小减速效果越明显
circleCount: 3, // 额外旋转圈数
showModal: true, // 是否显示结果弹窗
// 弹窗内容配置
modalContents: [
'恭喜您获得了奖品!',
'太棒了!您中奖了!',
'好运连连!'
],
// 指定中奖项配置
forceWinner: false, // 是否强制指定中奖项
winnerIndex: 0, // 指定的中奖项索引
// 事件日志
eventLogs: [],
// 中奖选项列表(用于选择器)
winnerOptions: [
{ index: -1, name: '随机抽取' }
]
};
},
created() {
// 初始化中奖选项列表
this.initWinnerOptions();
},
methods: {
/**
* 初始化中奖选项列表
*/
initWinnerOptions() {
// 先添加随机选项
this.winnerOptions = [{ index: -1, name: '随机抽取' }];
// 添加所有奖品选项
this.lotteryOptions.forEach((item, index) => {
this.winnerOptions.push({
index: index,
name: item.name
});
});
},
/**
* 设置中奖者的钩子函数
* @param {Array} options - 当前抽奖选项列表
* @returns {Number} - 返回中奖索引,-1表示随机抽取
*/
setWinner(options) {
// 记录日志
this.addLog('调用 setWinnerFn 钩子函数');
// 如果强制指定中奖项且索引有效,则返回指定索引
if (this.forceWinner && this.winnerIndex > 0) {
const selectedIndex = this.winnerOptions[this.winnerIndex].index;
this.addLog(`指定中奖项: ${options[selectedIndex].name}`);
return selectedIndex;
}
// 返回-1表示随机抽取
this.addLog('未指定中奖项,将随机抽取');
return -1;
},
/**
* 抽奖开始前的钩子函数
*/
onBeforePlay() {
this.addLog('抽奖开始');
},
/**
* 抽奖结束后的钩子函数
* @param {Object} result - 抽奖结果
*/
onAfterPlay(result) {
this.addLog(`抽奖结束,结果: ${result.name}`);
},
/**
* 重置转盘
*/
resetLottery() {
// 重新初始化转盘
this.$refs.lotteryRef.init();
this.addLog('转盘已重置');
},
/**
* 添加日志
* @param {String} message - 日志消息
*/
addLog(message) {
const now = new Date();
const timeStr = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`;
this.eventLogs.unshift(`[${timeStr}] ${message}`);
},
/**
* 清空日志
*/
clearLogs() {
this.eventLogs = [];
},
/**
* 中奖项选择变更
*/
onWinnerChange(e) {
this.winnerIndex = e.detail.value;
this.forceWinner = this.winnerIndex > 0;
if (this.forceWinner) {
const selectedIndex = this.winnerOptions[this.winnerIndex].index;
this.addLog(`已设置指定中奖项: ${this.lotteryOptions[selectedIndex].name}`);
} else {
this.addLog('已设置为随机抽取');
}
}
}
}
</script>
<style>
.container {
padding: 20rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.config-panel {
margin-top: 40rpx;
padding: 20rpx;
border-radius: 10rpx;
background-color: #f8f8f8;
}
.panel-title,
.log-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 20rpx;
border-bottom: 1px solid #eee;
padding-bottom: 10rpx;
}
.config-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.picker-value {
padding: 10rpx 20rpx;
background-color: #fff;
border-radius: 6rpx;
min-width: 200rpx;
text-align: center;
}
.reset-btn {
margin: 20rpx 0;
}
.event-log {
margin-top: 40rpx;
padding: 20rpx;
border-radius: 10rpx;
background-color: #f8f8f8;
}
.log-content {
height: 300rpx;
background-color: #fff;
padding: 10rpx;
border-radius: 6rpx;
margin-bottom: 20rpx;
}
.log-item {
font-size: 24rpx;
padding: 6rpx 0;
border-bottom: 1px dashed #eee;
}
</style>