<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>LuckyDu - 前端</title>
<link>https://blog.anlucky.cn/index.php/programming/web</link>
<atom:link href="https://blog.anlucky.cn/index.php/feed/programming/web" rel="self" type="application/rss+xml" />
<language>zh-CN</language>
<description>HTML，JavaScript，TypeScript，Vue2，Vue3，CSS，HTML</description>
<lastBuildDate>Tue, 15 Apr 2025 17:57:59 +0800</lastBuildDate>
<pubDate>Tue, 15 Apr 2025 17:57:59 +0800</pubDate>
<item>
<title>Uni-APP 组件-抽奖轮盘</title>
<link>https://blog.anlucky.cn/index.php/programming/web/236</link>
<guid>https://blog.anlucky.cn/index.php/programming/web/236</guid>
<pubDate>Tue, 15 Apr 2025 17:57:59 +0800</pubDate>
<dc:creator>都依凡</dc:creator>
<description><![CDATA[组件预览创建commponents/lottery/index.vue 文件输入如下代码轮盘抽检组件&lt;template&gt;    &lt;view class=&quot;main&q...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h2>组件预览</h2><p><img src="https://blog.anlucky.cn/usr/uploads/2025/04/1078851007.png" alt="image-20250415174320687.png" title="image-20250415174320687.png"></p><p>创建commponents/lottery/index.vue 文件</p><p>输入如下代码</p><h2>轮盘抽检组件</h2><pre><code class="lang-vue">&lt;template&gt;
    &lt;view class=&quot;main&quot;&gt;
        &lt;view v-if=&quot;options.length &gt; 0&quot; class=&quot;canvas-container&quot;&gt;
            &lt;canvas canvas-id=&quot;canvas&quot; id=&quot;canvas&quot; :style=&quot;canvasStyle&quot; /&gt;

            &lt;canvas canvas-id=&quot;canvasBtn&quot; id=&quot;canvasBtn&quot; class=&quot;canvasBtn&quot; :style=&quot;canvasStyle&quot; /&gt;

            &lt;cover-view @tap=&quot;playReward&quot; :class=&quot;[&#039;canvas-btn&#039;, { &#039;isLottery&#039;: isLottery }]&quot;
                :style=&quot;{ backgroundColor: centerBtnColor }&quot;&gt;
                {{ centerBtnText }}
            &lt;/cover-view&gt;
        &lt;/view&gt;
        &lt;view v-else class=&quot;empty-tip&quot;&gt;
            &lt;text&gt;暂无抽奖选项&lt;/text&gt;
        &lt;/view&gt;
        &lt;view v-if=&quot;showBtn&quot; class=&quot;btn-container&quot;&gt;
            &lt;button type=&quot;primary&quot; :disabled=&quot;isLottery&quot; @click=&quot;playReward&quot;&gt;{{ btnTitle }}&lt;/button&gt;
        &lt;/view&gt;
    &lt;/view&gt;
&lt;/template&gt;
&lt;script&gt;
var ctx = null;
export default {

    props: {
        // 弹窗内容 ，当 turnModalContent=【】时，默认弹窗内容为抽奖结果，并且不展示标题
        turnModalContent: {
            type: Array,
            default: () =&gt; []
        },
        // 减速，值越小，减速效果越明显 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: () =&gt; []
        },
        // 是否要展示开始按钮
        showBtn: {
            type: Boolean,
            default: false,
        },
        // 按钮文本
        btnTitle: {
            type: String,
            default: &quot;开始&quot;
        },
        // 是否显示结果弹窗
        showResultModal: {
            type: Boolean,
            default: true
        },
        // 中心按钮文本
        centerBtnText: {
            type: String,
            default: &quot;GO&quot;
        },
        // 中心按钮颜色
        centerBtnColor: {
            type: String,
            default: &quot;#ffffff&quot;
        }
    },
    data() {
        return {
            options: [{
                id: 1, // 唯一id
                name: &#039;水饺&#039;, // 名称
                // &quot;weight&quot;: 50, //  中奖权重，0-100
                img: &#039;&#039;, // 展示图片
                color: &quot;#f6e174&quot; // 轮盘区域底色
            },
            {
                id: 2,
                name: &#039;火锅&#039;,
                img: &#039;&#039;,
                color: &quot;#94494d&quot;
            },
            {
                id: 3,
                name: &#039;川菜&#039;,
                img: &#039;&#039;,
                color: &quot;#ffaa7f&quot;
            },
            {
                id: 4,
                name: &#039;麻辣烫&#039;,
                img: &#039;&#039;,
                color: &quot;#a48342&quot;
            },
            {
                id: 5,
                name: &#039;炸鸡汉堡&#039;,
                img: &#039;&#039;,
                color: &quot;#a25f81&quot;
            },
            {
                id: 6,
                name: &#039;重庆小面&#039;,
                img: &#039;&#039;,
                color: &quot;#98e2e2&quot;
            },
            {
                id: 7,
                name: &#039;酸辣粉&#039;,
                img: &#039;&#039;,
                color: &quot;#aaaa00&quot;
            },
            {
                id: 8,
                name: &#039;面条&#039;,
                img: &#039;&#039;,
                color: &quot;#648f76&quot;
            },
            {
                id: 9,
                name: &#039;汤河粉&#039;,
                img: &#039;&#039;,
                color: &quot;#40b3ff&quot;
            },
            {
                id: 10,
                name: &#039;猪脚饭&#039;,
                img: &#039;&#039;,
                color: &quot;#ffaaff&quot;
            },
            {
                id: 11,
                name: &#039;烧鸭饭&#039;,
                img: &#039;&#039;,
                color: &quot;#5555ff&quot;
            },
            {
                id: 12,
                name: &#039;白切鸡&#039;,
                img: &#039;&#039;,
                color: &quot;#ff90cf&quot;
            },
            {
                id: 13,
                name: &#039;烧鸡饭&#039;,
                img: &#039;&#039;,
                color: &quot;#55aa7f&quot;
            },
            {
                id: 14,
                name: &#039;肥肠面&#039;,
                img: &#039;&#039;,
                color: &quot;#4f95ff&quot;
            },
            {
                id: 15,
                name: &#039;炒饭&#039;,
                img: &#039;&#039;,
                color: &quot;#a509aa&quot;
            },
            {
                id: 16,
                name: &#039;黄焖鸡米饭&#039;,
                img: &#039;&#039;,
                color: &quot;#aaaa00&quot;
            },
            {
                id: 17,
                name: &#039;皮蛋瘦肉粥&#039;,
                img: &#039;&#039;,
                color: &quot;#ff52d1&quot;
            },
            {
                id: 18,
                name: &#039;沙县小吃&#039;,
                img: &#039;&#039;,
                color: &quot;#944ade&quot;
            },
            {
                id: 19,
                name: &#039;焖锅&#039;,
                img: &#039;&#039;,
                color: &quot;#43c900&quot;
            },
            {
                id: 20,
                name: &#039;披萨&#039;,
                img: &#039;&#039;,
                color: &quot;#7cbaba&quot;
            }
            ],
            isLottery: false, // 是否正在抽奖
            winnerIndex: -1, // 中奖索引
            isInitialized: false // 是否已初始化
        };
    },
    computed: {
        canvasStyle() {
            return &quot;width:&quot; + this.width + &quot;px;; height:&quot; + this.height + &quot;px;&quot;;
        },
    },
    watch: {
        // 监听数据变化，重新初始化
        data: {
            handler(newVal) {
                if (newVal &amp;&amp; newVal.length &gt; 0 &amp;&amp; !this.isUseDefaultOptions) {
                    this.init();
                }
            },
            deep: true
        }
    },
    methods: {
        // 处理文本，根据长度进行截断或调整
        processText(text, maxLength) {
            if (!text) return &#039;&#039;;

            // 如果文本长度超过最大长度，截断并添加省略号
            if (text.length &gt; maxLength) {
                return text.substring(0, maxLength - 2) + &#039;..&#039;;
            }
            return text;
        },
        // 获取文本显示的字体大小
        getTextFontSize(text, baseSize) {
            // 根据文本长度动态调整字体大小
            if (!text) return baseSize;

            if (text.length &gt; 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(&quot;抽奖选项为空，无法开始抽奖&quot;);
                return;
            }
            this.isLottery = true
            console.log(&quot;开始抽奖&quot;)
            // 触发beforePlay事件
            if (this.beforePlay) {
                this.beforePlay();
            }
            this.$emit(&quot;beforePlay&quot;);
            // 获取中奖索引
            let num = -1;
            // 使用setWinnerFn获取中奖索引
            if (this.setWinnerFn &amp;&amp; typeof this.setWinnerFn === &#039;function&#039;) {
                try {
                    // 修复调用方式
                    num = this.setWinnerFn(this.options);
                    if (num === undefined || num &lt; 0 || num &gt;= len) {
                        console.warn(&quot;setWinnerFn返回的索引无效，将使用随机抽奖&quot;);
                        num = -1;
                    }
                } catch (error) {
                    console.error(&quot;调用setWinnerFn出错:&quot;, error);
                    num = -1;
                }
            }
            if (num &lt; 0) {
                // 进行权重抽奖
                let optionIndex = this.lotteryWeight(this.options)
                if (optionIndex !== undefined &amp;&amp; optionIndex &gt;= 0) {
                    num = optionIndex
                } else {
                    // 没有自动抽奖结果 &amp;&amp; 没有定义选项权重，则进行随机数抽奖
                    num = Math.floor(Math.random() * len)
                }
            }
            this.winnerIndex = num;
            try {
                const result = await this.roateCanvas(num)
                console.log(&quot;抽奖结果：&quot;, result.name)
                this.isLottery = false
                // 是否显示结果弹窗
                if (this.showResultModal) {
                    let title = &quot;&quot;
                    let content = result.name
                    if (this.turnModalContent.length &gt; 0) {
                        // 若设置了 content ，则随机取一个 content 内容，并且 title 将展示为抽奖结果 
                        title = result.name
                        content = this.turnModalContent[Math.floor(Math.random() * this.turnModalContent.length)]
                    }
                    uni.showModal({
                        confirmText: &quot;确定&quot;,
                        showCancel: false,
                        title: title,
                        content: content,
                        success: (res) =&gt; {
                            if (res.confirm) {
                                console.log(&#039;用户点击确定&#039;);
                                // 调用钩子函数
                                if (this.afterPlay) {
                                    this.afterPlay(result);
                                }
                                this.$emit(&quot;afterPlay&quot;, result)
                            }
                        }
                    })
                } else {
                    // 直接触发afterPlay事件
                    if (this.afterPlay) {
                        this.afterPlay(result);
                    }
                    this.$emit(&quot;afterPlay&quot;, result)
                }
            } catch (error) {
                console.error(&quot;抽奖过程出错:&quot;, error);
                this.isLottery = false;
                uni.showToast({
                    title: &#039;抽奖失败，请重试&#039;,
                    icon: &#039;none&#039;
                });
            }

        },

        initBtnCanvas() {
            try {
                let angleTo = 0
                let ctx = uni.createCanvasContext(&quot;canvasBtn&quot;, 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(&quot;#ff0000&quot;);
                ctx.fill();
                ctx.draw();
            } catch (error) {
                console.error(&quot;初始化按钮画布失败:&quot;, error);
            }
        },
        initCanvas: function (ctx, angleTo) {
            try {
                const len = this.options.length; //数组长度
                if (len == 0) {
                    console.warn(&quot;options len == 0&quot;);
                    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(&quot;#ffffff&quot;);
                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 &lt; 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 &lt; len; i++) {
                    // 根据奖项数量和文本长度调整文本位置和大小
                    let textDistance = -(center_x / 2) - 25;

                    // 奖项过多时，文本更靠近中心
                    if (len &gt; 10) {
                        textDistance = -(center_x / 2) - 15;
                    }
                    if (len &gt; 15) {
                        textDistance = -(center_x / 2) - 5;
                    }

                    // 处理文本，根据奖项数量决定最大长度
                    let maxTextLength = len &gt; 12 ? 4 : (len &gt; 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(&quot;center&quot;);
                    ctx.setFillStyle(&quot;#FFFFFF&quot;);
                    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(&quot;#FFFFFF&quot;);
                ctx.fill();
                ctx.draw();

                this.isInitialized = true;
            } catch (error) {
                console.error(&quot;初始化画布失败:&quot;, error);
                this.isInitialized = false;
            }
        },
        // 根据设置的权重进行抽奖
        lotteryWeight(prizes) {
            console.log(&quot;进行权重抽奖&quot;)
            if (!prizes || prizes.length === 0) {
                console.log(&quot;奖品列表为空&quot;)
                return -1
            }

            // 计算总权重和有效奖品
            let totalWeight = 0
            const validPrizes = []

            // 筛选有效奖品并计算总权重
            for (let index = 0; index &lt; prizes.length; index++) {
                const prize = prizes[index]
                const weight = Number(prize.weight)

                // 只有权重大于0的奖品才参与抽奖
                if (weight &amp;&amp; weight &gt; 0) {
                    totalWeight += weight
                    validPrizes.push({
                        index,
                        weight,
                        name: prize.name
                    })
                }
            }

            // 如果没有有效奖品，返回-1表示随机抽奖
            if (validPrizes.length === 0 || totalWeight === 0) {
                console.log(&quot;没有设置有效权重的奖品，将进行随机抽奖&quot;)
                return -1
            }

            // 生成0到总权重之间的随机数
            const random = Math.random() * totalWeight
            console.log(&quot;权重抽奖随机数:&quot;, random, &quot;总权重:&quot;, totalWeight)

            // 根据权重区间确定中奖奖品
            let accumulatedWeight = 0
            for (const prize of validPrizes) {
                accumulatedWeight += prize.weight
                if (random &lt; accumulatedWeight) {
                    console.log(&quot;权重抽奖奖品 &quot;, prizes[prize.index].name, &quot; 中奖，权重:&quot;, prize.weight)
                    return prize.index
                }
            }
            // 保底返回最后一个有效奖品
            console.log(&quot;未找到匹配奖品，进行随机抽奖&quot;)
            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 &gt; 0) {
                let turnCircle = this.turnCircle
                // 最多只能转 10 圈
                if (turnCircle &gt; 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) =&gt; {
                let timer = setInterval(function () {
                    // console.log(&quot;count = &quot;， count);
                    that.initCanvas(that.ctx, count);
                    if (count == angle) {
                        clearInterval(timer);
                        result = that.options[num];
                        resolve(result)
                    }
                    count = count + baseStep * (((angle - count) / angle) &gt; baseSpeed ? baseSpeed :
                        ((angle -
                            count) / angle)) + 0.1;

                    if (angle - count &lt; 0.5) {
                        count = angle;
                    }
                }, 25);
            });
        },
        getFontSize() {
            let fontSize = this.fontSize
            if (this.options.length &gt; 10) {
                if (this.fontSize &gt;= 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 = &quot;#&quot; + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b);

            return hexColor;
        },
        componentToHex(c) {
            var hex = c.toString(16);
            return hex.length === 1 ? &quot;0&quot; + hex : hex;
        },
        initOptions: function () {
            try {
                let defaultOptions = JSON.parse(JSON.stringify(this.options)); // 深拷贝默认选项

                if (this.isUseDefaultOptions) {
                    // 使用默认数据
                    console.log(&quot;使用默认选项数据&quot;);
                } else if (this.data &amp;&amp; this.data.length &gt; 0) {
                    console.log(&quot;使用传入的选项数据, 长度:&quot;, this.data.length);
                    this.options = JSON.parse(JSON.stringify(this.data)); // 深拷贝传入数据
                } else {
                    console.warn(&quot;未提供有效的选项数据，将使用默认选项&quot;);
                }

                // 所有默认的颜色
                let allDefColorArr = defaultOptions.map(item =&gt; item.color);

                // 找到最大的 id
                let maxId = 0;
                this.options.forEach(item =&gt; {
                    if (item.id &amp;&amp; item.id &gt; maxId) {
                        maxId = item.id;
                    }
                });

                // 填充 id，确保 id 唯一
                var existIds = []
                for (var i = 0; i &lt; this.options.length; i++) {
                    let item = this.options[i]
                    if (item[&#039;id&#039;] == undefined || item.id == 0 || existIds.includes(item.id)) {
                        // id 不存在，或者 id 重复了
                        maxId++;
                        this.options[i][&#039;id&#039;] = maxId;
                    }
                    existIds.push(this.options[i].id)
                }

                // 填充颜色，确保颜色唯一
                let availableColor = JSON.parse(JSON.stringify(allDefColorArr)) // 可用颜色
                let existColor = [] // 存在颜色

                // 先收集已有颜色
                for (var i = 0; i &lt; this.options.length; i++) {
                    let item = this.options[i]
                    if (item[&#039;color&#039;] &amp;&amp; item.color !== &quot;&quot;) {
                        let color = item.color
                        existColor.push(color)
                        // 过滤掉已经用了的 color
                        availableColor = availableColor.filter(c =&gt; c !== color);
                    }
                }

                // 为没有颜色的选项分配颜色
                for (var i = 0; i &lt; this.options.length; i++) {
                    let item = this.options[i]
                    if (!item[&#039;color&#039;] || item.color === &quot;&quot;) {
                        if (availableColor.length == 0) {
                            // 没有可用颜色了，则随机生成一个
                            let color = &#039;&#039;;
                            for (var j = 0; j &lt; 100; j++) {
                                if (color !== &#039;&#039;) break;
                                let genColor = this.genRandColor()
                                if (!existColor.includes(genColor)) {
                                    existColor.push(genColor)
                                    color = genColor
                                }
                            }
                            this.options[i][&#039;color&#039;] = color
                        } else {
                            const color = availableColor.shift();
                            existColor.push(color)
                            this.options[i].color = color
                        }
                    }
                }

                console.log(&quot;选项初始化完成，数量:&quot;, this.options.length);
            } catch (error) {
                console.error(&quot;初始化选项失败:&quot;, error);
                // 确保至少有默认选项
                if (!this.options || this.options.length === 0) {
                    this.options = [{
                        id: 1,
                        name: &#039;默认选项&#039;,
                        color: &quot;#f6e174&quot;
                    }];
                }
            }
        },
        init() {
            try {
                this.initOptions();
                this.ctx = uni.createCanvasContext(&quot;canvas&quot;, this);
                this.initCanvas(this.ctx, 0);
                this.initBtnCanvas();
            } catch (error) {
                console.error(&quot;初始化组件失败:&quot;, error);
                uni.showToast({
                    title: &#039;初始化抽奖组件失败&#039;,
                    icon: &#039;none&#039;
                });
            }
        }
    },
    // 初始化画布
    mounted: function () {
        console.log(&quot;lottery mounted init......&quot;)
        this.init()
    }
}
&lt;/script&gt;
&lt;style scoped&gt;
.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;
}
&lt;/style&gt;</code></pre><h2>测试Demo</h2><pre><code class="lang-vue">&lt;template&gt;
    &lt;view class=&quot;container&quot;&gt;
        &lt;view class=&quot;title&quot;&gt;转盘抽奖组件演示&lt;/view&gt;

        &lt;!-- 转盘抽奖组件 --&gt;
        &lt;lottery ref=&quot;lotteryRef&quot; :width=&quot;300&quot; :height=&quot;300&quot; :fontSize=&quot;16&quot; :data=&quot;lotteryOptions&quot;
            :isUseDefaultOptions=&quot;useDefaultOptions&quot; :turnReduceSpeed=&quot;reduceSpeed&quot; :turnCircle=&quot;circleCount&quot;
            :showBtn=&quot;true&quot; :btnTitle=&quot;&#039;开始抽奖&#039;&quot; :centerBtnText=&quot;&#039;GO&#039;&quot; :centerBtnColor=&quot;&#039;#ff5252&#039;&quot;
            :showResultModal=&quot;showModal&quot; :turnModalContent=&quot;modalContents&quot; :setWinnerFn=&quot;setWinner&quot;
            :beforePlay=&quot;onBeforePlay&quot; :afterPlay=&quot;onAfterPlay&quot; /&gt;

        &lt;!-- 配置面板 --&gt;
        &lt;view class=&quot;config-panel&quot;&gt;
            &lt;view class=&quot;panel-title&quot;&gt;配置选项&lt;/view&gt;

            &lt;view class=&quot;config-item&quot;&gt;
                &lt;text&gt;使用默认选项:&lt;/text&gt;
                &lt;switch :checked=&quot;useDefaultOptions&quot; @change=&quot;(e) =&gt; useDefaultOptions = e.detail.value&quot; /&gt;
            &lt;/view&gt;

            &lt;view class=&quot;config-item&quot;&gt;
                &lt;text&gt;减速效果({{ reduceSpeed }}):&lt;/text&gt;
                &lt;slider :value=&quot;reduceSpeed&quot; :min=&quot;1&quot; :max=&quot;100&quot; :step=&quot;1&quot; show-value
                    @change=&quot;(e) =&gt; reduceSpeed = e.detail.value&quot; /&gt;
            &lt;/view&gt;

            &lt;view class=&quot;config-item&quot;&gt;
                &lt;text&gt;额外旋转圈数({{ circleCount }}):&lt;/text&gt;
                &lt;slider :value=&quot;circleCount&quot; :min=&quot;0&quot; :max=&quot;10&quot; :step=&quot;1&quot; show-value
                    @change=&quot;(e) =&gt; circleCount = e.detail.value&quot; /&gt;
            &lt;/view&gt;

            &lt;view class=&quot;config-item&quot;&gt;
                &lt;text&gt;显示结果弹窗:&lt;/text&gt;
                &lt;switch :checked=&quot;showModal&quot; @change=&quot;(e) =&gt; showModal = e.detail.value&quot; /&gt;
            &lt;/view&gt;

            &lt;view class=&quot;config-item&quot;&gt;
                &lt;text&gt;指定中奖项:&lt;/text&gt;
                &lt;picker :value=&quot;winnerIndex&quot; :range=&quot;winnerOptions&quot; range-key=&quot;name&quot; @change=&quot;onWinnerChange&quot;&gt;
                    &lt;view class=&quot;picker-value&quot;&gt;{{ winnerOptions[winnerIndex].name || &#039;随机&#039; }}&lt;/view&gt;
                &lt;/picker&gt;
            &lt;/view&gt;

            &lt;button class=&quot;reset-btn&quot; type=&quot;default&quot; @click=&quot;resetLottery&quot;&gt;重置转盘&lt;/button&gt;
        &lt;/view&gt;

        &lt;!-- 事件日志 --&gt;
        &lt;view class=&quot;event-log&quot;&gt;
            &lt;view class=&quot;log-title&quot;&gt;事件日志&lt;/view&gt;
            &lt;scroll-view scroll-y class=&quot;log-content&quot;&gt;
                &lt;view v-for=&quot;(log, index) in eventLogs&quot; :key=&quot;index&quot; class=&quot;log-item&quot;&gt;
                    {{ log }}
                &lt;/view&gt;
            &lt;/scroll-view&gt;
            &lt;button size=&quot;mini&quot; type=&quot;default&quot; @click=&quot;clearLogs&quot;&gt;清空日志&lt;/button&gt;
        &lt;/view&gt;
    &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
import Lottery from &#039;@/components/lottery/index.vue&#039;

export default {
    // 注册组件
    components: {
        Lottery
    },
    data() {
        return {
            // 转盘选项数据
            lotteryOptions: [
                { id: 1, name: &#039;一等奖&#039;, weight: 5, color: &#039;#ff4444&#039; },
                { id: 2, name: &#039;二等奖&#039;, weight: 10, color: &#039;#ff8800&#039; },
                { id: 3, name: &#039;三等奖&#039;, weight: 15, color: &#039;#ffcc00&#039; },
                { id: 4, name: &#039;四等奖&#039;, weight: 20, color: &#039;#33cc33&#039; },
                { id: 5, name: &#039;五等奖&#039;, weight: 25, color: &#039;#3399ff&#039; },
                { id: 6, name: &#039;谢谢参与&#039;, weight: 25, color: &#039;#9966ff&#039; }
            ],

            // 配置选项
            useDefaultOptions: false, // 是否使用组件默认选项
            reduceSpeed: 50, // 减速效果，值越小减速效果越明显
            circleCount: 3, // 额外旋转圈数
            showModal: true, // 是否显示结果弹窗

            // 弹窗内容配置
            modalContents: [
                &#039;恭喜您获得了奖品！&#039;,
                &#039;太棒了！您中奖了！&#039;,
                &#039;好运连连！&#039;
            ],

            // 指定中奖项配置
            forceWinner: false, // 是否强制指定中奖项
            winnerIndex: 0, // 指定的中奖项索引

            // 事件日志
            eventLogs: [],

            // 中奖选项列表（用于选择器）
            winnerOptions: [
                { index: -1, name: &#039;随机抽取&#039; }
            ]
        };
    },

    created() {
        // 初始化中奖选项列表
        this.initWinnerOptions();
    },

    methods: {
        /**
         * 初始化中奖选项列表
         */
        initWinnerOptions() {
            // 先添加随机选项
            this.winnerOptions = [{ index: -1, name: &#039;随机抽取&#039; }];

            // 添加所有奖品选项
            this.lotteryOptions.forEach((item, index) =&gt; {
                this.winnerOptions.push({
                    index: index,
                    name: item.name
                });
            });
        },

        /**
         * 设置中奖者的钩子函数
         * @param {Array} options - 当前抽奖选项列表
         * @returns {Number} - 返回中奖索引，-1表示随机抽取
         */
        setWinner(options) {
            // 记录日志
            this.addLog(&#039;调用 setWinnerFn 钩子函数&#039;);

            // 如果强制指定中奖项且索引有效，则返回指定索引
            if (this.forceWinner &amp;&amp; this.winnerIndex &gt; 0) {
                const selectedIndex = this.winnerOptions[this.winnerIndex].index;
                this.addLog(`指定中奖项: ${options[selectedIndex].name}`);
                return selectedIndex;
            }

            // 返回-1表示随机抽取
            this.addLog(&#039;未指定中奖项，将随机抽取&#039;);
            return -1;
        },

        /**
         * 抽奖开始前的钩子函数
         */
        onBeforePlay() {
            this.addLog(&#039;抽奖开始&#039;);
        },

        /**
         * 抽奖结束后的钩子函数
         * @param {Object} result - 抽奖结果
         */
        onAfterPlay(result) {
            this.addLog(`抽奖结束，结果: ${result.name}`);
        },

        /**
         * 重置转盘
         */
        resetLottery() {
            // 重新初始化转盘
            this.$refs.lotteryRef.init();
            this.addLog(&#039;转盘已重置&#039;);
        },

        /**
         * 添加日志
         * @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 &gt; 0;

            if (this.forceWinner) {
                const selectedIndex = this.winnerOptions[this.winnerIndex].index;
                this.addLog(`已设置指定中奖项: ${this.lotteryOptions[selectedIndex].name}`);
            } else {
                this.addLog(&#039;已设置为随机抽取&#039;);
            }
        }
    }
}
&lt;/script&gt;

&lt;style&gt;
.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;
}
&lt;/style&gt;</code></pre>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.anlucky.cn/index.php/programming/web/236#comments</comments>
<wfw:commentRss>https://blog.anlucky.cn/index.php/feed/programming/web</wfw:commentRss>
</item>
</channel>
</rss>