After Width: | Height: | Size: 40 KiB |
@ -0,0 +1,552 @@ |
|||||||
|
<template> |
||||||
|
<view class="almost-lottery"> |
||||||
|
<!-- head --> |
||||||
|
<image src="../static/lottery/top-bg@3x.jpg" mode="widthFix" class="width100"></image> |
||||||
|
<view class="almost-lottery__wheel"> |
||||||
|
<almost-lottery :lottery-size="lotteryConfig.lotterySize" :action-size="lotteryConfig.actionSize" |
||||||
|
:ring-count="2" :duration="1" :prize-list="prizeList" :prize-index="prizeIndex" |
||||||
|
@reset-index="prizeIndex = -1" @draw-start="handleDrawStart" @draw-end="handleDrawEnd" |
||||||
|
@finish="handleDrawFinish" v-if="prizeList.length" /> |
||||||
|
</view> |
||||||
|
<view class="almost-lottery__rule"> |
||||||
|
<view class="rule-head"> |
||||||
|
<view class="line"></view> |
||||||
|
<text class="title">中奖名单</text> |
||||||
|
<view class="line"></view> |
||||||
|
</view> |
||||||
|
<!-- |
||||||
|
<view style="height: 40px;"> |
||||||
|
<view class="page-section swiper"> |
||||||
|
<view class="page-section-spacing"> |
||||||
|
<swiper class="swiper" vertical="true" :indicator-dots="indicatorDots" :autoplay="autoplay" :interval="interval" :duration="duration"> |
||||||
|
<swiper-item style="height: 40px;"> |
||||||
|
<view class="swiper-item uni-bg-red">冯绍峰地方是的反手反手电风扇电风扇发是</view> |
||||||
|
</swiper-item> |
||||||
|
<swiper-item style="height: 40px;"> |
||||||
|
<view class="swiper-item uni-bg-green">了看了你别浪费女生了妇女了啥地方了南斯拉夫弄丢了</view> |
||||||
|
</swiper-item> |
||||||
|
<swiper-item style="height: 40px;"> |
||||||
|
<view class="swiper-item uni-bg-blue">沃尔沃二女玩女人温柔我能让你为免费派送费牛棚</view> |
||||||
|
</swiper-item> |
||||||
|
</swiper> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> --> |
||||||
|
|
||||||
|
<view class="rule-head"> |
||||||
|
<view class="line"></view> |
||||||
|
<text class="title">活动规则</text> |
||||||
|
<view class="line"></view> |
||||||
|
</view> |
||||||
|
<view class="rule-body"> |
||||||
|
<view class="item"> |
||||||
|
<view class="number">1</view> |
||||||
|
<view class="text"> |
||||||
|
<text>抽奖细则:</text> |
||||||
|
<text>每人每天拥有{{ freeNumDay }}次抽奖机会,每次消耗{{ goldNum }}金币。</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="item item-rule"> |
||||||
|
<view class="number">2</view> |
||||||
|
<view class="text"> |
||||||
|
<text>奖励说明:</text> |
||||||
|
<text style="margin-top: 1px;">1.一等奖1000元加油券(4名)</text> |
||||||
|
<text style="margin-top: 1px;">2.二等奖500元加油券(12名)</text> |
||||||
|
<text style="margin-top: 1px;">3.三等奖300元加油券(20名)</text> |
||||||
|
<text style="margin-top: 1px;">4.参与奖20元(200-20)抵扣券(2000名)</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<template> |
||||||
|
<view class="item"> |
||||||
|
<view class="number">3</view> |
||||||
|
<view class="text">本次活动由XXXXXXX发起,与Apple.Inc无关。</view> |
||||||
|
</view> |
||||||
|
<view class="item"> |
||||||
|
<view class="number">4</view> |
||||||
|
<view class="text">苹果公司不是本活动的赞助商且没有以任何形式参与活动。</view> |
||||||
|
</view> |
||||||
|
<view class="item"> |
||||||
|
<view class="number">5</view> |
||||||
|
<view class="text">本活动仅限17岁以上用户参加。</view> |
||||||
|
</view> |
||||||
|
<view class="item"> |
||||||
|
<view class="number">6</view> |
||||||
|
<view class="text">本活动最终解释权归XXXXXXX所有。</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import AlmostLottery from '../../uni_modules/almost-lottery/components/almost-lottery/almost-lottery.vue' |
||||||
|
export default { |
||||||
|
name: 'Home', |
||||||
|
components: { |
||||||
|
AlmostLottery |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
// 开启调试模式 |
||||||
|
isDev: true, |
||||||
|
background: ['color1', 'color2', 'color3'], |
||||||
|
indicatorDots: false, |
||||||
|
autoplay: true, |
||||||
|
interval: 2000, |
||||||
|
duration: 500, |
||||||
|
// 以下是转盘配置相关数据 |
||||||
|
lotteryConfig: { |
||||||
|
// 抽奖转盘的整体尺寸,单位rpx |
||||||
|
lotterySize: 600, |
||||||
|
// 抽奖按钮的尺寸,单位rpx |
||||||
|
actionSize: 200 |
||||||
|
}, |
||||||
|
|
||||||
|
// 以下是转盘 UI 配置 |
||||||
|
// 转盘外环图 |
||||||
|
lotteryBg: 'https://raw.githubusercontent.com/ialmost/almost-components_uniapp/dev/Lottery/almost-lottery_sample/static/lottery-bg.png', |
||||||
|
// 抽奖按钮图 |
||||||
|
actionBg: 'https://raw.githubusercontent.com/ialmost/almost-components_uniapp/dev/Lottery/almost-lottery_sample/static/action-bg.png', |
||||||
|
|
||||||
|
// 以下是奖品配置数据 |
||||||
|
// 奖品数据 |
||||||
|
prizeList: [], |
||||||
|
// 奖品是否设有库存 |
||||||
|
onStock: true, |
||||||
|
// 中奖下标 |
||||||
|
prizeIndex: -1, |
||||||
|
|
||||||
|
// 是否正在抽奖中,避免重复触发 |
||||||
|
prizeing: false, |
||||||
|
|
||||||
|
// 以下为中奖概率有关数据 |
||||||
|
// 是否由前端控制概率,默认不开启,强烈建议由后端控制 |
||||||
|
onFrontend: false, |
||||||
|
// 权重随机数的最大值 |
||||||
|
prizeWeightMax: 0, |
||||||
|
// 权重数组 |
||||||
|
prizeWeightArr: [], |
||||||
|
|
||||||
|
// 以下为业务需求有关示例数据 |
||||||
|
// 金币余额 |
||||||
|
goldCoin: 600, |
||||||
|
// 当日免费抽奖次数余额 |
||||||
|
freeNum: 3, |
||||||
|
// 每次消耗的金币数 |
||||||
|
goldNum: 20, |
||||||
|
// 每天免费抽奖次数 |
||||||
|
freeNumDay: 3 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
isApple() { |
||||||
|
return uni.getSystemInfoSync().platform === 'ios' |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
|
||||||
|
changeIndicatorDots(e) { |
||||||
|
this.indicatorDots = !this.indicatorDots |
||||||
|
}, |
||||||
|
changeAutoplay(e) { |
||||||
|
this.autoplay = !this.autoplay |
||||||
|
}, |
||||||
|
intervalChange(e) { |
||||||
|
this.interval = e.target.value |
||||||
|
}, |
||||||
|
durationChange(e) { |
||||||
|
this.duration = e.target.value |
||||||
|
}, |
||||||
|
// 重新生成 |
||||||
|
handleInitCanvas() { |
||||||
|
|
||||||
|
|
||||||
|
this.prizeList = [] |
||||||
|
this.getPrizeList() |
||||||
|
}, |
||||||
|
// 获取奖品列表 |
||||||
|
async getPrizeList() { |
||||||
|
uni.showLoading({ |
||||||
|
title: '奖品准备中...' |
||||||
|
}) |
||||||
|
|
||||||
|
// 等待接口返回的数据进一步处理 |
||||||
|
let res = await this.requestApiGetPrizeList() |
||||||
|
console.log('获取奖品列表', res) |
||||||
|
|
||||||
|
if (res.ok) { |
||||||
|
let data = res.data |
||||||
|
if (data.length) { |
||||||
|
this.prizeList = data |
||||||
|
console.log('已获取到奖品列表数据,开始绘制抽奖转盘') |
||||||
|
|
||||||
|
// 如果开启了前端控制概率 |
||||||
|
// 得出权重的最大值并生成权重数组 |
||||||
|
if (this.onFrontend) { |
||||||
|
// 生成权重数组并排序取得最大值 |
||||||
|
this.prizeWeightArr = this.prizeList.map((item) => item.prizeWeight) |
||||||
|
let prizeWeightArrSort = [...this.prizeWeightArr] |
||||||
|
prizeWeightArrSort.sort((a, b) => b - a) |
||||||
|
this.prizeWeightMax = prizeWeightArrSort[0] |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
uni.hideLoading() |
||||||
|
uni.showToast({ |
||||||
|
title: '获取奖品失败', |
||||||
|
mask: true, |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 模拟请求 获取奖品列表 接口, |
||||||
|
// 注意这里返回的是一个 Promise |
||||||
|
// 大哥,这里只是模拟,别告诉我你不会对接自己的接口 |
||||||
|
requestApiGetPrizeList() { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
let requestTimer = setTimeout(() => { |
||||||
|
clearTimeout(requestTimer) |
||||||
|
requestTimer = null |
||||||
|
|
||||||
|
// prizeStock 奖品库存 |
||||||
|
// prizeWeight 中奖概率,数值越大中奖概率越高,权重一样时随机中奖 |
||||||
|
resolve({ |
||||||
|
ok: true, |
||||||
|
data: [{ |
||||||
|
prizeId: 1, |
||||||
|
prizeName: '0.1元现金', |
||||||
|
prizeStock: 10, |
||||||
|
prizeWeight: 200, |
||||||
|
prizeImage: '/static/img/order3.png' |
||||||
|
}, |
||||||
|
{ |
||||||
|
prizeId: 2, |
||||||
|
prizeName: '10元现金', |
||||||
|
prizeStock: 0, |
||||||
|
prizeWeight: 50, |
||||||
|
prizeImage: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/56f085e0-bcfe-11ea-b244-a9f5e5565f30.png' |
||||||
|
}, |
||||||
|
{ |
||||||
|
prizeId: 3, |
||||||
|
prizeName: '5元话费', |
||||||
|
prizeStock: 1, |
||||||
|
prizeWeight: 80, |
||||||
|
prizeImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAABCElEQVRoge3YMa4BURSH8Y8o7UAp0WgkotBZwluAfhqlZSgUGr23ENUUCpppJnTswAIUSCaTiziZJ8d9/193zdzrfMltABF5plb+oLscDoAV0Pn8OC/lwDhL0k35QT3wstcIuM61Cj0IhXiNuAvOFwr5SgrxRiHeKMSbhnHfAVgU1i1gajhnBpwK6wnQtgxkDTlmSTq/L7rLYQ9byG+WpLvCOT8YQ6K5WgrxRiHeKMQbhXijEG8U4o1CvIkmxPrDquwMrI37KlFJSJake2BUxVlW0VytaEKsV6t5+8Ohak3rRmtIH9hav/QvRHO1FOKNQrwJheQfn+I9wflCIeNHLzuQc51PRP6rC1ZeIm1I8cC5AAAAAElFTkSuQmCC' |
||||||
|
}, |
||||||
|
{ |
||||||
|
prizeId: 4, |
||||||
|
prizeName: '50元现金', |
||||||
|
prizeStock: 0, |
||||||
|
prizeWeight: 10, |
||||||
|
prizeImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAB2klEQVRIia3Wv09TURjG8U+u2kQZcNENh2ok0cnJ0R+L0RB2g/EvwITEwRgnV0Zx0fAHYBh0khBD4sBAmAgDiwQNg4qwmSBDAYf7Fi+n97al9UlObnvO836f3tO355b2GsAYZrCBPRzGdSPmx8J3YtXwDDsB7TR2wl/rNmAYK13C07ES9W11E9s9BjTHdnBKVf8PAcWgehpwCssF0w/5FzqLgw7AA7zDQ/wqzC8H90jjSeFMYW0Yr8PzALfjOh7z1wve2YTzpLlQw2ay+KZ1N7vS24SziVqGexhKzB07pEJXk/dDwTetdZ9HewwZLWFNw2oyudZjQFNrCW8109pqS32GpPX1TOu586fPkLR+IEMjmbzYZ0ha34Atx/fwW58hXxPeFixo7Yg7PQbcKmEtZFgsMb/C4AkDBuUnQKpFuFZI3cZuvP6C+8g6wDP572O95C4Og49/W/YJl/G5YPqO9yV3dh4f8LMCfhjcI93Afix8xAX5SVx2YBY10yagEdxjmiwY7uISXuIFrlSEPG0TMllWcAbzYViXd0rzeXC6ImSiImA+eKU6h7mCeV/eCBMnCJkLzpHSztnFCJ7L//ZkOFv1iRLtRd1IcLpSHVP4jccVnkexPqXkmd7UX15b7tiz29ReAAAAAElFTkSuQmCC' |
||||||
|
}, |
||||||
|
{ |
||||||
|
prizeId: 5, |
||||||
|
prizeName: '1卷抽纸', |
||||||
|
prizeStock: 3, |
||||||
|
prizeWeight: 3000, |
||||||
|
prizeImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAABCElEQVRoge3YMa4BURSH8Y8o7UAp0WgkotBZwluAfhqlZSgUGr23ENUUCpppJnTswAIUSCaTiziZJ8d9/193zdzrfMltABF5plb+oLscDoAV0Pn8OC/lwDhL0k35QT3wstcIuM61Cj0IhXiNuAvOFwr5SgrxRiHeKMSbhnHfAVgU1i1gajhnBpwK6wnQtgxkDTlmSTq/L7rLYQ9byG+WpLvCOT8YQ6K5WgrxRiHeKMQbhXijEG8U4o1CvIkmxPrDquwMrI37KlFJSJake2BUxVlW0VytaEKsV6t5+8Ohak3rRmtIH9hav/QvRHO1FOKNQrwJheQfn+I9wflCIeNHLzuQc51PRP6rC1ZeIm1I8cC5AAAAAElFTkSuQmCC' |
||||||
|
}, |
||||||
|
{ |
||||||
|
prizeId: 6, |
||||||
|
prizeName: '0.2元现金', |
||||||
|
prizeStock: 8, |
||||||
|
prizeWeight: 120, |
||||||
|
prizeImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAABCElEQVRoge3YMa4BURSH8Y8o7UAp0WgkotBZwluAfhqlZSgUGr23ENUUCpppJnTswAIUSCaTiziZJ8d9/193zdzrfMltABF5plb+oLscDoAV0Pn8OC/lwDhL0k35QT3wstcIuM61Cj0IhXiNuAvOFwr5SgrxRiHeKMSbhnHfAVgU1i1gajhnBpwK6wnQtgxkDTlmSTq/L7rLYQ9byG+WpLvCOT8YQ6K5WgrxRiHeKMQbhXijEG8U4o1CvIkmxPrDquwMrI37KlFJSJake2BUxVlW0VytaEKsV6t5+8Ohak3rRmtIH9hav/QvRHO1FOKNQrwJheQfn+I9wflCIeNHLzuQc51PRP6rC1ZeIm1I8cC5AAAAAElFTkSuQmCC' |
||||||
|
}, |
||||||
|
{ |
||||||
|
prizeId: 7, |
||||||
|
prizeName: '谢谢参与', |
||||||
|
prizeStock: 100, |
||||||
|
prizeWeight: 10000, |
||||||
|
prizeImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAABCElEQVRoge3YMa4BURSH8Y8o7UAp0WgkotBZwluAfhqlZSgUGr23ENUUCpppJnTswAIUSCaTiziZJ8d9/193zdzrfMltABF5plb+oLscDoAV0Pn8OC/lwDhL0k35QT3wstcIuM61Cj0IhXiNuAvOFwr5SgrxRiHeKMSbhnHfAVgU1i1gajhnBpwK6wnQtgxkDTlmSTq/L7rLYQ9byG+WpLvCOT8YQ6K5WgrxRiHeKMQbhXijEG8U4o1CvIkmxPrDquwMrI37KlFJSJake2BUxVlW0VytaEKsV6t5+8Ohak3rRmtIH9hav/QvRHO1FOKNQrwJheQfn+I9wflCIeNHLzuQc51PRP6rC1ZeIm1I8cC5AAAAAElFTkSuQmCC' |
||||||
|
}, |
||||||
|
{ |
||||||
|
prizeId: 8, |
||||||
|
prizeName: '100金币', |
||||||
|
prizeStock: 100, |
||||||
|
prizeWeight: 3000, |
||||||
|
prizeImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAABCElEQVRoge3YMa4BURSH8Y8o7UAp0WgkotBZwluAfhqlZSgUGr23ENUUCpppJnTswAIUSCaTiziZJ8d9/193zdzrfMltABF5plb+oLscDoAV0Pn8OC/lwDhL0k35QT3wstcIuM61Cj0IhXiNuAvOFwr5SgrxRiHeKMSbhnHfAVgU1i1gajhnBpwK6wnQtgxkDTlmSTq/L7rLYQ9byG+WpLvCOT8YQ6K5WgrxRiHeKMQbhXijEG8U4o1CvIkmxPrDquwMrI37KlFJSJake2BUxVlW0VytaEKsV6t5+8Ohak3rRmtIH9hav/QvRHO1FOKNQrwJheQfn+I9wflCIeNHLzuQc51PRP6rC1ZeIm1I8cC5AAAAAElFTkSuQmCC' |
||||||
|
} |
||||||
|
] |
||||||
|
}) |
||||||
|
}, 2000) |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 本次抽奖开始 |
||||||
|
handleDrawStart() { |
||||||
|
console.log('触发抽奖按钮') |
||||||
|
if (this.prizeing) return |
||||||
|
this.prizeing = true |
||||||
|
|
||||||
|
// 还有免费数次或者剩余金币足够抽一次 |
||||||
|
if (this.freeNum > 0 || this.goldCoin >= this.goldNum) { |
||||||
|
|
||||||
|
// 更新免费次数或金币余额 |
||||||
|
if (this.freeNum > 0) { |
||||||
|
this.freeNum-- |
||||||
|
} else if (this.goldCoin >= this.goldNum) { |
||||||
|
this.goldCoin -= this.goldNum |
||||||
|
} |
||||||
|
|
||||||
|
this.tryLotteryDraw() |
||||||
|
} else { |
||||||
|
this.prizeing = false |
||||||
|
uni.showModal({ |
||||||
|
title: '金币不足', |
||||||
|
content: '是否前往赚取金币?', |
||||||
|
success: (res) => { |
||||||
|
// 这里需要根据业务需求处理,一般情况下会引导用户前往赚取金币的页面 |
||||||
|
console.log('金币不足', res) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 尝试发起抽奖 |
||||||
|
tryLotteryDraw() { |
||||||
|
console.log('旋转开始,获取中奖下标......') |
||||||
|
// 判断是否由前端控制概率 |
||||||
|
if (this.onFrontend) { |
||||||
|
this.localGetPrizeIndex() |
||||||
|
} else { |
||||||
|
this.remoteGetPrizeIndex() |
||||||
|
} |
||||||
|
}, |
||||||
|
// 本地获取中奖下标 |
||||||
|
localGetPrizeIndex() { |
||||||
|
console.warn('###当前处于前端控制中奖概率,安全起见,强烈建议由后端控制###') |
||||||
|
// 前端控制概率的情况下,需要拿到最接近随机权重且大于随机权重的值 |
||||||
|
// 后端控制概率的情况下,通常会直接返回 prizeId |
||||||
|
if (!this.prizeWeightMax) { |
||||||
|
console.warn('###当前已开启前端控制中奖概率,但是奖品数据列表中的 prizeWeight 参数似乎配置不正确###') |
||||||
|
return |
||||||
|
} |
||||||
|
console.log('当前权重最大值为 =>', this.prizeWeightMax) |
||||||
|
|
||||||
|
// 注意这里使用了 Math.ceil,如果某个权重的值为 0,则始终无法中奖 |
||||||
|
let randomWeight = Math.ceil(Math.random() * this.prizeWeightMax) |
||||||
|
console.log('本次权重随机数 =>', randomWeight) |
||||||
|
|
||||||
|
// 生成大于等于随机权重的数组 |
||||||
|
let tempMaxArrs = [] |
||||||
|
this.prizeList.forEach((item) => { |
||||||
|
if (item.prizeWeight >= randomWeight) { |
||||||
|
tempMaxArrs.push(item.prizeWeight) |
||||||
|
} |
||||||
|
}) |
||||||
|
console.log('tempMaxArrs', tempMaxArrs) |
||||||
|
|
||||||
|
// 如果大于随机权重的数组有值,先对这个数组排序然后取值 |
||||||
|
// 反之新建一个临时的包含所有权重的已排序数组,然后取值 |
||||||
|
let tempMaxArrsLen = tempMaxArrs.length |
||||||
|
if (tempMaxArrsLen) { |
||||||
|
tempMaxArrs.sort((a, b) => a - b) |
||||||
|
// 取值时,如果存在多个值,分两种情况 |
||||||
|
if (tempMaxArrsLen > 1) { |
||||||
|
// 检查是否存在重复的值 |
||||||
|
let sameCount = 0 |
||||||
|
for (let i = 0; i < tempMaxArrs.length; i++) { |
||||||
|
if (tempMaxArrs[i] === tempMaxArrs[0]) { |
||||||
|
sameCount++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 值不相等的情况下取最接近的值,也就是第1个值 |
||||||
|
if (sameCount === 1) { |
||||||
|
this.prizeIndex = this.prizeWeightArr.indexOf(tempMaxArrs[0]) |
||||||
|
} else { |
||||||
|
// 存在值相等时,随机取值,当然这里你可以自己决定是否随机取值 |
||||||
|
let sameWeight = tempMaxArrs[0] |
||||||
|
let sameWeightArr = [] |
||||||
|
let sameWeightItem = {} |
||||||
|
this.prizeWeightArr.forEach((item, index) => { |
||||||
|
if (item === sameWeight) { |
||||||
|
sameWeightArr.push({ |
||||||
|
prizeWeight: item, |
||||||
|
index |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
console.log('sameWeightArr', sameWeightArr) |
||||||
|
sameWeightItem = sameWeightArr[Math.floor(Math.random() * sameWeightArr.length)] |
||||||
|
console.log('sameWeightItem', sameWeightItem) |
||||||
|
this.prizeIndex = sameWeightItem.index |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.prizeIndex = this.prizeWeightArr.indexOf(tempMaxArrs[0]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
console.log('本次抽中奖品 =>', this.prizeList[this.prizeIndex].prizeName) |
||||||
|
|
||||||
|
// 如果奖品设有库存 |
||||||
|
if (this.onStock) { |
||||||
|
console.log('本次奖品库存 =>', this.prizeList[this.prizeIndex].prizeStock) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 远程请求接口获取中奖下标 |
||||||
|
// 大哥,这里只是模拟,别告诉我你不会对接自己的接口 |
||||||
|
remoteGetPrizeIndex() { |
||||||
|
console.warn('###当前处于模拟的请求接口,并返回了中奖信息###') |
||||||
|
// 模拟请求接口获取中奖信息 |
||||||
|
let stoTimer = setTimeout(() => { |
||||||
|
clearTimeout(stoTimer) |
||||||
|
stoTimer = null |
||||||
|
|
||||||
|
let list = [...this.prizeList] |
||||||
|
|
||||||
|
// 这里随机产生的 prizeId 是模拟后端返回的 prizeId |
||||||
|
let prizeId = Math.floor(Math.random() * list.length + 1) |
||||||
|
|
||||||
|
// 拿到后端返回的 prizeId 后,开始循环比对得出那个中奖的数据 |
||||||
|
for (let i = 0; i < list.length; i++) { |
||||||
|
let item = list[i] |
||||||
|
if (item.prizeId === prizeId) { |
||||||
|
// 中奖下标 |
||||||
|
this.prizeIndex = i |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
console.log('本次抽中奖品 =>', this.prizeList[this.prizeIndex].prizeName) |
||||||
|
|
||||||
|
// 如果奖品设有库存 |
||||||
|
if (this.onStock) { |
||||||
|
console.log('本次奖品库存 =>', this.prizeList[this.prizeIndex].prizeStock) |
||||||
|
} |
||||||
|
}, 200) |
||||||
|
}, |
||||||
|
// 本次抽奖结束 |
||||||
|
handleDrawEnd() { |
||||||
|
console.log('旋转结束,执行拿到结果后到逻辑') |
||||||
|
|
||||||
|
// 旋转结束后,开始处理拿到结果后的逻辑 |
||||||
|
this.prizeing = false |
||||||
|
let prizeName = this.prizeList[this.prizeIndex].prizeName |
||||||
|
let tipContent = '' |
||||||
|
|
||||||
|
if (prizeName === '谢谢参与') { |
||||||
|
tipContent = '很遗憾,没有中奖,请再接再厉!' |
||||||
|
} else { |
||||||
|
// 如果奖品设有库存 |
||||||
|
if (this.onStock) { |
||||||
|
let prizeStock = this.prizeList[this.prizeIndex].prizeStock |
||||||
|
tipContent = prizeStock ? `恭喜您,获得 ${prizeName}` : `很抱歉,您来晚了,当前奖品 ${prizeName} 已无库存` |
||||||
|
} else { |
||||||
|
tipContent = `恭喜您,获得 ${prizeName} !` |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
uni.showModal({ |
||||||
|
content: tipContent, |
||||||
|
showCancel: false |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 抽奖转盘绘制完成 |
||||||
|
handleDrawFinish(res) { |
||||||
|
console.log('抽奖转盘绘制完成', res) |
||||||
|
|
||||||
|
uni.showToast({ |
||||||
|
title: res.msg, |
||||||
|
mask: true, |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
onLoad() { |
||||||
|
this.handleInitCanvas() |
||||||
|
}, |
||||||
|
onUnload() { |
||||||
|
uni.hideLoading() |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.almost-lottery { |
||||||
|
flex: 1; |
||||||
|
background-color: #FF893F; |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__wheel { |
||||||
|
margin-top: 30rpx; |
||||||
|
text-align: center; |
||||||
|
|
||||||
|
.almost-lottery__count { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
text-align: center; |
||||||
|
padding: 40rpx 0; |
||||||
|
} |
||||||
|
|
||||||
|
.text { |
||||||
|
color: #FFFFFF; |
||||||
|
font-size: 24rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__rule { |
||||||
|
padding: 0 28rpx; |
||||||
|
color: #FFF8CB; |
||||||
|
|
||||||
|
.rule-head { |
||||||
|
display: flex; |
||||||
|
justify-content: space-around; |
||||||
|
align-items: center; |
||||||
|
margin: 40rpx 0; |
||||||
|
|
||||||
|
.line { |
||||||
|
flex: 1; |
||||||
|
height: 1px; |
||||||
|
background-color: #FFF3A5; |
||||||
|
} |
||||||
|
|
||||||
|
.title { |
||||||
|
width: 280rpx; |
||||||
|
color: #F63857; |
||||||
|
line-height: 70rpx; |
||||||
|
text-align: center; |
||||||
|
margin: 0 20rpx; |
||||||
|
border-radius: 8rpx; |
||||||
|
background-image: linear-gradient(0deg, rgba(255, 242, 158, 1), rgba(255, 244, 168, 1)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.rule-body { |
||||||
|
color: #FFF8CB; |
||||||
|
font-size: 24rpx; |
||||||
|
padding: 10rpx 0 40rpx; |
||||||
|
|
||||||
|
.item { |
||||||
|
display: flex; |
||||||
|
margin-bottom: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.number { |
||||||
|
position: relative; |
||||||
|
top: 4rpx; |
||||||
|
width: 28rpx; |
||||||
|
height: 28rpx; |
||||||
|
line-height: 28rpx; |
||||||
|
text-align: center; |
||||||
|
border-radius: 50%; |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.text { |
||||||
|
flex: 1; |
||||||
|
margin-top: 1px; |
||||||
|
} |
||||||
|
|
||||||
|
.item-rule .text { |
||||||
|
display: flex; |
||||||
|
margin-top: 1px; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 232 KiB |
After Width: | Height: | Size: 214 KiB |
@ -0,0 +1,132 @@ |
|||||||
|
## 1.8.5(2021-09-13) |
||||||
|
本次更新: |
||||||
|
- 修复一个已知问题 |
||||||
|
## 1.8.4(2021-09-12) |
||||||
|
本次更新: |
||||||
|
- 调整 `strFontColor` 为 `strFontColors`,现在可以设置每个区块的文字颜色,详见文档说明 |
||||||
|
## 1.8.3(2021-09-12) |
||||||
|
本次更新: |
||||||
|
- 修复因 `1.8.0` 改动引起的文字方向、无奖品图时绘制异常的问题 |
||||||
|
- 新增 `imgDrawed` 是否绘制奖品图片的配置项 ,默认为 `true` |
||||||
|
## 1.8.2(2021-09-10) |
||||||
|
本次更新: |
||||||
|
**不兼容旧版本的更新** |
||||||
|
- 移除配置项 `strKey` 字段 |
||||||
|
- 调整 `prizeList` 结构 |
||||||
|
## 1.8.1(2021-09-06) |
||||||
|
本次更新: |
||||||
|
- 修复 `hbx3.1.22` 在小程序平台处理 `id-name` 存在解析错误的问题 |
||||||
|
## 1.8.0(2021-09-06) |
||||||
|
本次更新: |
||||||
|
**该版本更新涉及破坏性的变更,请重新查看 `API - Props` 的部分** |
||||||
|
- `px` 全面调整为 `rpx` 单位,多个`Props` 的参数相应调整,请查看文档 |
||||||
|
- 新增 `pixelRatio` 参数,该参数为设计稿的设备像素比基准值,默认为 `2` 倍素 |
||||||
|
## 1.7.18(2021-09-05) |
||||||
|
本次更新: |
||||||
|
- 修复一个已知问题 |
||||||
|
## 1.7.17(2021-08-23) |
||||||
|
本次更新: |
||||||
|
- 更新示例项目 |
||||||
|
## 1.7.16(2021-08-14) |
||||||
|
本次更新: |
||||||
|
- 更新示例项目 |
||||||
|
## 1.7.15(2021-08-03) |
||||||
|
本次更新: |
||||||
|
- 新增文字竖向展示的功能,详见文档说明 |
||||||
|
## 1.7.13(2021-08-02) |
||||||
|
本次更新: |
||||||
|
- 更新文档 |
||||||
|
## 1.7.12(2021-07-30) |
||||||
|
本次更新: |
||||||
|
- 修复示例项目的已知问题 |
||||||
|
- 现已提供Almost-Lottery抽奖转盘的uniCloud云端一体页面模板 |
||||||
|
- 现已提供Almost-Lottery抽奖转盘云端一体页面配套的Admin配置中心 |
||||||
|
## 1.7.11(2021-07-22) |
||||||
|
本次更新: |
||||||
|
- 修复部分安卓手机文字大小异常的问题 |
||||||
|
- 字段 `strHeightMultiple` 更换为 `strLineHeight` |
||||||
|
## 1.7.10(2021-07-12) |
||||||
|
本次更新: |
||||||
|
- 修复奖品名称 `name` 为空字符串时无法成功绘制转盘的问题 |
||||||
|
- 新增 `prizeNameDrawed` 是否绘制奖品名称的配置项,现在可以仅展示奖品图片了 |
||||||
|
## 1.7.9(2021-07-09) |
||||||
|
本次更新: |
||||||
|
- 优化组件内部代码 |
||||||
|
- 修复奖品图片已然是 `base64` 格式时导致转盘绘制失败的问题 |
||||||
|
- 文档新增QQ群号,让沟通更便捷 |
||||||
|
## 1.7.8(2021-07-08) |
||||||
|
本次更新: |
||||||
|
- 调整 `Canvas` 默认宽高为 `280` |
||||||
|
## 1.7.7(2021-07-08) |
||||||
|
本次更新: |
||||||
|
- 新增多个配置项,满足更多自定义需求 |
||||||
|
- 优化多行文本情况下非中文字符的字节处理 |
||||||
|
- 修复偶发的第一条数据文本不居中显示的问题 |
||||||
|
## 1.7.6(2021-07-02) |
||||||
|
本次更新: |
||||||
|
- 调整 `imageWidth` 、 `imageHeight` 字段为 `imgWidth` 、 `imgHeight` |
||||||
|
- 更新示例项目 |
||||||
|
## 1.7.5(2021-07-01) |
||||||
|
本次更新: |
||||||
|
- 新增配置项 `imgMarginStr` 奖品图片距离奖品文字的距离 |
||||||
|
## 1.7.4(2021-06-28) |
||||||
|
本次更新: |
||||||
|
- 新增轮盘旋转或指针旋转配置项 |
||||||
|
- 转盘内置的外环图片以及按钮图片统一调整为 `image` 展示 |
||||||
|
- 更新相关文档说明 |
||||||
|
## 1.7.3(2021-06-16) |
||||||
|
本次更新: |
||||||
|
- 优化错误提示 |
||||||
|
- 优化示例项目 |
||||||
|
- 优化文档说明 |
||||||
|
## 1.7.2(2021-06-11) |
||||||
|
本次更新: |
||||||
|
- 新增 `canvasId` 参数配置项,多画板情况下需要配置不同的 `canvasId` |
||||||
|
- 优化多画板情况下的缓存功能 |
||||||
|
- 优化示例项目 |
||||||
|
- 修改文档说明 |
||||||
|
## 1.7.1(2021-06-10) |
||||||
|
本次更新: |
||||||
|
- 优化示例项目中的注释 |
||||||
|
## 1.7.0(2021-06-04) |
||||||
|
本次更新: |
||||||
|
- 修复 `1.6.1` 引起的多行奖品文字行高异常的问题 |
||||||
|
- 新增配置转盘外环和抽奖按钮图片的功能,详见文档说明 |
||||||
|
- 更新示例项目,新增抽奖次数等业务有关的逻辑供参考 |
||||||
|
## 1.6.1(2021-05-28) |
||||||
|
本次更新: |
||||||
|
- 修复小程序平台画板模糊的问题 |
||||||
|
## 1.6.0(2021-05-28) |
||||||
|
本次更新: |
||||||
|
- 新增奖品区块是否开启描边的配置项,默认不开启 |
||||||
|
- 调整画板缓存为默认不开启 |
||||||
|
- 优化代码 |
||||||
|
- 优化文档说明 |
||||||
|
- 更新示例项目并修改部分注释 |
||||||
|
## 1.5.13(2021-05-22) |
||||||
|
本次更新: |
||||||
|
- 优化文档说明 |
||||||
|
- 更新示例项目 |
||||||
|
## 1.5.12(2021-05-22) |
||||||
|
本次更新: |
||||||
|
- 新增配置项 `strokeColor` 奖品区块边框颜色 |
||||||
|
- 更新文档说明 |
||||||
|
## 1.5.11(2021-05-19) |
||||||
|
本次更新: |
||||||
|
- 新增`strMarginOutside`参数,用于设置奖品文字距离边缘的距离 |
||||||
|
- 修复奖品文字在某些情况下不是居中显示的问题 |
||||||
|
## 1.5.10(2021-05-19) |
||||||
|
本次更新: |
||||||
|
- 修复示例项目中权重值相同时的取值逻辑 |
||||||
|
## 1.5.9(2021-05-14) |
||||||
|
本次更新: |
||||||
|
- 调整代码,优化小程序端的展示 |
||||||
|
## 1.5.8(2021-05-12) |
||||||
|
本次更新: |
||||||
|
- 文档增加预警提示:不再维护非 `uni_modules` 模式下的版本 |
||||||
|
## 1.5.7(2021-05-12) |
||||||
|
本次更新: |
||||||
|
- 修复小程序平台奖品名称不清晰的问题 |
||||||
|
## 1.5.6(2021-03-18) |
||||||
|
本次更新: |
||||||
|
- 适配 uni_modules 插件模式 |
@ -0,0 +1,970 @@ |
|||||||
|
<template> |
||||||
|
<view class="almost-lottery"> |
||||||
|
<view class="almost-lottery__wrap" :style="{ width: lotterySize + 'rpx', height: lotterySize + 'rpx' }"> |
||||||
|
<view class="lottery-action" :style="{ width: actionSize + 'rpx', height: actionSize + 'rpx', left: canvasMarginOutside + 'rpx' }"></view> |
||||||
|
<view class="str-margin-outside" :style="{ left: strMarginOutside + 'rpx' }"></view> |
||||||
|
<view class="img-margin-str" :style="{ left: imgMarginStr + 'rpx' }"></view> |
||||||
|
<view class="img-size" :style="{ width: imgWidth + 'rpx', height: imgHeight + 'rpx' }"></view> |
||||||
|
<template v-if="lotteryImg"> |
||||||
|
<image |
||||||
|
class="almost-lottery__bg" |
||||||
|
mode="widthFix" |
||||||
|
:src="lotteryBg" |
||||||
|
:style="{ |
||||||
|
width: lotteryPxSize + 'px', |
||||||
|
height: lotteryPxSize + 'px' |
||||||
|
}" |
||||||
|
></image> |
||||||
|
<image |
||||||
|
class="almost-lottery__canvas-img" |
||||||
|
mode="widthFix" |
||||||
|
:src="lotteryImg" |
||||||
|
:style="{ |
||||||
|
width: canvasPxSize + 'px', |
||||||
|
height: canvasPxSize + 'px', |
||||||
|
transform: `rotate(${canvasAngle + targetAngle}deg)`, |
||||||
|
transitionDuration: `${transitionDuration}s` |
||||||
|
}" |
||||||
|
></image> |
||||||
|
<image |
||||||
|
class="almost-lottery__action almost-lottery__action-bg" |
||||||
|
mode="widthFix" |
||||||
|
:src="actionBg" |
||||||
|
:style="{ |
||||||
|
width: actionPxSize + 'px', |
||||||
|
height: actionPxSize + 'px', |
||||||
|
transform: `rotate(${actionAngle + targetActionAngle}deg)`, |
||||||
|
transitionDuration: `${transitionDuration}s` |
||||||
|
}" |
||||||
|
@click="handleActionStart" |
||||||
|
></image> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 正在绘制转盘时的提示文本 --> |
||||||
|
<text class="almost-lottery__tip" v-else>{{ almostLotteryTip }}</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 为了兼容 app 端 ctx.measureText 所需的标签 --> |
||||||
|
<text class="almost-lottery__measureText" :style="{ fontSize: higtFontSize + 'px' }">{{ measureText }}</text> |
||||||
|
|
||||||
|
<!-- #ifdef MP-ALIPAY --> |
||||||
|
<canvas |
||||||
|
:class="className" |
||||||
|
:id="canvasId" |
||||||
|
:width="higtCanvasSize" |
||||||
|
:height="higtCanvasSize" |
||||||
|
:style="{ |
||||||
|
width: higtCanvasSize + 'px', |
||||||
|
height: higtCanvasSize + 'px' |
||||||
|
}" |
||||||
|
/> |
||||||
|
<!-- #endif --> |
||||||
|
<!-- #ifndef MP-ALIPAY --> |
||||||
|
<canvas |
||||||
|
:class="className" |
||||||
|
:canvas-id="canvasId" |
||||||
|
:width="higtCanvasSize" |
||||||
|
:height="higtCanvasSize" |
||||||
|
:style="{ |
||||||
|
width: higtCanvasSize + 'px', |
||||||
|
height: higtCanvasSize + 'px' |
||||||
|
}" |
||||||
|
/> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
const systemInfo = uni.getSystemInfoSync() |
||||||
|
import { getStore, setStore, clearStore, clacTextLen, downloadFile, pathToBase64 } from '@/uni_modules/almost-lottery/utils/almost-utils.js' |
||||||
|
export default { |
||||||
|
name: 'AlmostLottery', |
||||||
|
props: { |
||||||
|
// 设计稿的像素比基准值 |
||||||
|
pixelRatio: { |
||||||
|
type: Number, |
||||||
|
default: 2 |
||||||
|
}, |
||||||
|
// canvas 标识 |
||||||
|
canvasId: { |
||||||
|
type: String, |
||||||
|
default: 'almostLottery' |
||||||
|
}, |
||||||
|
// 抽奖转盘的整体尺寸 |
||||||
|
lotterySize: { |
||||||
|
type: Number, |
||||||
|
default: 600 |
||||||
|
}, |
||||||
|
// 抽奖按钮的尺寸 |
||||||
|
actionSize: { |
||||||
|
type: Number, |
||||||
|
default: 200 |
||||||
|
}, |
||||||
|
// canvas边缘距离转盘边缘的距离 |
||||||
|
canvasMarginOutside: { |
||||||
|
type: Number, |
||||||
|
default: 90 |
||||||
|
}, |
||||||
|
// 奖品列表 |
||||||
|
prizeList: { |
||||||
|
type: Array, |
||||||
|
required: true, |
||||||
|
validator: (value) => { |
||||||
|
return value.length > 1 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 中奖奖品在列表中的下标 |
||||||
|
prizeIndex: { |
||||||
|
type: Number, |
||||||
|
required: true |
||||||
|
}, |
||||||
|
// 奖品区块对应背景颜色 |
||||||
|
colors: { |
||||||
|
type: Array, |
||||||
|
default: () => [ |
||||||
|
'#FFFFFF', |
||||||
|
'#FFBF05' |
||||||
|
] |
||||||
|
}, |
||||||
|
// 转盘外环背景图 |
||||||
|
lotteryBg: { |
||||||
|
type: String, |
||||||
|
default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg2x.png' |
||||||
|
}, |
||||||
|
// 抽奖按钮背景图 |
||||||
|
actionBg: { |
||||||
|
type: String, |
||||||
|
default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action2x.png' |
||||||
|
}, |
||||||
|
// 是否绘制奖品名称 |
||||||
|
prizeNameDrawed: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
// 是否开启奖品区块描边 |
||||||
|
stroked: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
// 描边颜色 |
||||||
|
strokeColor: { |
||||||
|
type: String, |
||||||
|
default: '#FFBF05' |
||||||
|
}, |
||||||
|
// 旋转的类型 |
||||||
|
rotateType: { |
||||||
|
type: String, |
||||||
|
default: 'roulette' |
||||||
|
}, |
||||||
|
// 旋转动画时间 单位s |
||||||
|
duration: { |
||||||
|
type: Number, |
||||||
|
default: 8 |
||||||
|
}, |
||||||
|
// 旋转的圈数 |
||||||
|
ringCount: { |
||||||
|
type: Number, |
||||||
|
default: 8 |
||||||
|
}, |
||||||
|
// 指针位置 |
||||||
|
pointerPosition: { |
||||||
|
type: String, |
||||||
|
default: 'edge', |
||||||
|
validator: (value) => { |
||||||
|
return value === 'edge' || value === 'middle' |
||||||
|
} |
||||||
|
}, |
||||||
|
// 文字方向 |
||||||
|
strDirection: { |
||||||
|
type: String, |
||||||
|
default: 'horizontal', |
||||||
|
validator: (value) => { |
||||||
|
return value === 'horizontal' || value === 'vertical' |
||||||
|
} |
||||||
|
}, |
||||||
|
// 字体颜色 |
||||||
|
strFontColors: { |
||||||
|
type: Array, |
||||||
|
default: () => [ |
||||||
|
'#FFBF05', |
||||||
|
'#FFFFFF' |
||||||
|
] |
||||||
|
}, |
||||||
|
// 文字的大小 |
||||||
|
strFontSize: { |
||||||
|
type: Number, |
||||||
|
default: 24 |
||||||
|
}, |
||||||
|
// 奖品文字距离边缘的距离 |
||||||
|
strMarginOutside: { |
||||||
|
type: Number, |
||||||
|
default: 0 |
||||||
|
}, |
||||||
|
// 奖品图片距离奖品文字的距离 |
||||||
|
imgMarginStr: { |
||||||
|
type: Number, |
||||||
|
default: 60 |
||||||
|
}, |
||||||
|
// 奖品文字多行情况下的行高 |
||||||
|
strLineHeight: { |
||||||
|
type: Number, |
||||||
|
default: 1.2 |
||||||
|
}, |
||||||
|
// 奖品文字总长度限制 |
||||||
|
strMaxLen: { |
||||||
|
type: Number, |
||||||
|
default: 12 |
||||||
|
}, |
||||||
|
// 奖品文字多行情况下第一行文字长度 |
||||||
|
strLineLen: { |
||||||
|
type: Number, |
||||||
|
default: 6 |
||||||
|
}, |
||||||
|
// 奖品图片的宽 |
||||||
|
imgWidth: { |
||||||
|
type: Number, |
||||||
|
default: 50 |
||||||
|
}, |
||||||
|
// 奖品图片的高 |
||||||
|
imgHeight: { |
||||||
|
type: Number, |
||||||
|
default: 50 |
||||||
|
}, |
||||||
|
// 是否绘制奖品图片 |
||||||
|
imgDrawed: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
// 转盘绘制成功的提示 |
||||||
|
successMsg: { |
||||||
|
type: String, |
||||||
|
default: '奖品准备就绪,快来参与抽奖吧' |
||||||
|
}, |
||||||
|
// 转盘绘制失败的提示 |
||||||
|
failMsg: { |
||||||
|
type: String, |
||||||
|
default: '奖品仍在准备中,请稍后再来...' |
||||||
|
}, |
||||||
|
// 是否开启画板的缓存 |
||||||
|
canvasCached: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
// 画板className |
||||||
|
className: 'almost-lottery__canvas', |
||||||
|
// 抽奖转盘的整体px尺寸 |
||||||
|
lotteryPxSize: 0, |
||||||
|
// 画板的px尺寸 |
||||||
|
canvasPxSize: 0, |
||||||
|
// 抽奖按钮的px尺寸 |
||||||
|
actionPxSize: 0, |
||||||
|
// 奖品文字距离转盘边缘的距离 |
||||||
|
strMarginPxOutside: 0, |
||||||
|
// 奖品图片相对奖品文字的距离 |
||||||
|
imgMarginPxStr: 0, |
||||||
|
// 奖品图片的宽、高 |
||||||
|
imgPxWidth: 0, |
||||||
|
imgPxHeight: 0, |
||||||
|
// 画板导出的图片 |
||||||
|
lotteryImg: '', |
||||||
|
// 旋转到奖品目标需要的角度 |
||||||
|
targetAngle: 0, |
||||||
|
targetActionAngle: 0, |
||||||
|
// 旋转动画时间 单位 s |
||||||
|
transitionDuration: 0, |
||||||
|
// 是否正在旋转 |
||||||
|
isRotate: false, |
||||||
|
// 当前停留在那个奖品的序号 |
||||||
|
stayIndex: 0, |
||||||
|
// 当前中奖奖品的序号 |
||||||
|
targetIndex: 0, |
||||||
|
// 是否存在可用的缓存转盘图 |
||||||
|
isCacheImg: false, |
||||||
|
oldLotteryImg: '', |
||||||
|
// 转盘绘制时的提示 |
||||||
|
almostLotteryTip: '奖品准备中...', |
||||||
|
// 解决 app 不支持 measureText 的问题 |
||||||
|
// app 已在 2.9.3 的版本中提供了对 measureText 的支持,将在后续版本逐渐稳定后移除相关兼容代码 |
||||||
|
measureText: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 高清尺寸 |
||||||
|
higtCanvasSize() { |
||||||
|
return this.canvasPxSize * systemInfo.pixelRatio |
||||||
|
}, |
||||||
|
// 高清字体 |
||||||
|
higtFontSize() { |
||||||
|
return (this.strFontSize / this.pixelRatio) * systemInfo.pixelRatio |
||||||
|
}, |
||||||
|
// 高清行高 |
||||||
|
higtHeightMultiple() { |
||||||
|
return (this.strFontSize / this.pixelRatio) * this.strLineHeight * systemInfo.pixelRatio |
||||||
|
}, |
||||||
|
// 根据奖品列表计算 canvas 旋转角度 |
||||||
|
canvasAngle() { |
||||||
|
let result = 0 |
||||||
|
|
||||||
|
let prizeCount = this.prizeList.length |
||||||
|
let prizeClip = 360 / prizeCount |
||||||
|
let diffNum = 90 / prizeClip |
||||||
|
if (this.pointerPosition === 'edge' || this.rotateType === 'pointer') { |
||||||
|
result = -(prizeClip * diffNum) |
||||||
|
} else { |
||||||
|
result = -(prizeClip * diffNum + prizeClip / 2) |
||||||
|
} |
||||||
|
return result |
||||||
|
}, |
||||||
|
actionAngle() { |
||||||
|
return 0 |
||||||
|
}, |
||||||
|
// 外圆的半径 |
||||||
|
outsideRadius() { |
||||||
|
return this.higtCanvasSize / 2 |
||||||
|
}, |
||||||
|
// 内圆的半径 |
||||||
|
insideRadius() { |
||||||
|
return 20 * systemInfo.pixelRatio |
||||||
|
}, |
||||||
|
// 文字距离边缘的距离 |
||||||
|
textRadius() { |
||||||
|
return this.strMarginPxOutside * systemInfo.pixelRatio || (this.higtFontSize / 2) |
||||||
|
}, |
||||||
|
// 根据画板的宽度计算奖品文字与中心点的距离 |
||||||
|
textDistance() { |
||||||
|
const textZeroY = Math.round(this.outsideRadius - (this.insideRadius / 2)) |
||||||
|
return textZeroY - this.textRadius |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
// 监听获奖序号的变动 |
||||||
|
prizeIndex(newVal, oldVal) { |
||||||
|
if (newVal > -1) { |
||||||
|
this.targetIndex = newVal |
||||||
|
this.onRotateStart() |
||||||
|
} else { |
||||||
|
console.info('旋转结束,prizeIndex 已重置') |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 开始旋转 |
||||||
|
onRotateStart() { |
||||||
|
if (this.isRotate) return |
||||||
|
this.isRotate = true |
||||||
|
// 奖品总数 |
||||||
|
let prizeCount = this.prizeList.length |
||||||
|
let baseAngle = 360 / prizeCount |
||||||
|
let angles = 0 |
||||||
|
|
||||||
|
if (this.rotateType === 'pointer') { |
||||||
|
if (this.targetActionAngle === 0) { |
||||||
|
// 第一次旋转 |
||||||
|
angles = (this.targetIndex - this.stayIndex) * baseAngle + baseAngle / 2 - this.actionAngle |
||||||
|
} else { |
||||||
|
// 后续旋转 |
||||||
|
// 后续继续旋转 就只需要计算停留的位置与目标位置的角度 |
||||||
|
angles = (this.targetIndex - this.stayIndex) * baseAngle |
||||||
|
} |
||||||
|
|
||||||
|
// 更新目前序号 |
||||||
|
this.stayIndex = this.targetIndex |
||||||
|
// 转 8 圈,圈数越多,转的越快 |
||||||
|
this.targetActionAngle += angles + 360 * this.ringCount |
||||||
|
console.log('targetActionAngle', this.targetActionAngle) |
||||||
|
} else { |
||||||
|
if (this.targetAngle === 0) { |
||||||
|
// 第一次旋转 |
||||||
|
// 因为第一个奖品是从0°开始的,即水平向右方向 |
||||||
|
// 第一次旋转角度 = 270度 - (停留的序号-目标序号) * 每个奖品区间角度 - 每个奖品区间角度的一半 - canvas自身旋转的度数 |
||||||
|
angles = (270 - (this.targetIndex - this.stayIndex) * baseAngle - baseAngle / 2) - this.canvasAngle |
||||||
|
} else { |
||||||
|
// 后续旋转 |
||||||
|
// 后续继续旋转 就只需要计算停留的位置与目标位置的角度 |
||||||
|
angles = -(this.targetIndex - this.stayIndex) * baseAngle |
||||||
|
} |
||||||
|
|
||||||
|
// 更新目前序号 |
||||||
|
this.stayIndex = this.targetIndex |
||||||
|
// 转 8 圈,圈数越多,转的越快 |
||||||
|
this.targetAngle += angles + 360 * this.ringCount |
||||||
|
} |
||||||
|
|
||||||
|
// 计算转盘结束的时间,预加一些延迟确保转盘停止后触发结束事件 |
||||||
|
let endTime = this.transitionDuration * 1000 + 100 |
||||||
|
let endTimer = setTimeout(() => { |
||||||
|
clearTimeout(endTimer) |
||||||
|
endTimer = null |
||||||
|
|
||||||
|
this.isRotate = false |
||||||
|
this.$emit('draw-end') |
||||||
|
}, endTime) |
||||||
|
|
||||||
|
let resetPrizeTimer = setTimeout(() => { |
||||||
|
clearTimeout(resetPrizeTimer) |
||||||
|
resetPrizeTimer = null |
||||||
|
|
||||||
|
// 每次抽奖结束后都要重置父级组件的 prizeIndex |
||||||
|
this.$emit('reset-index') |
||||||
|
}, endTime + 50) |
||||||
|
}, |
||||||
|
// 点击 开始抽奖 按钮 |
||||||
|
handleActionStart() { |
||||||
|
if (!this.lotteryImg) return |
||||||
|
if (this.isRotate) return |
||||||
|
this.$emit('draw-start') |
||||||
|
}, |
||||||
|
// 渲染转盘 |
||||||
|
async onCreateCanvas() { |
||||||
|
// 获取 canvas 画布 |
||||||
|
const canvasId = this.canvasId |
||||||
|
const ctx = uni.createCanvasContext(canvasId, this) |
||||||
|
|
||||||
|
// canvas 的宽高 |
||||||
|
let canvasW = this.higtCanvasSize |
||||||
|
let canvasH = this.higtCanvasSize |
||||||
|
|
||||||
|
// 根据奖品个数计算 角度 |
||||||
|
let prizeCount = this.prizeList.length |
||||||
|
let baseAngle = Math.PI * 2 / prizeCount |
||||||
|
|
||||||
|
// 设置字体 |
||||||
|
ctx.setFontSize(this.higtFontSize) |
||||||
|
|
||||||
|
// 注意,开始画的位置是从0°角的位置开始画的。也就是水平向右的方向。 |
||||||
|
// 画具体内容 |
||||||
|
for (let i = 0; i < prizeCount; i++) { |
||||||
|
let prizeItem = this.prizeList[i] |
||||||
|
// 当前角度 |
||||||
|
let angle = i * baseAngle |
||||||
|
|
||||||
|
// 保存当前画布的状态 |
||||||
|
ctx.save() |
||||||
|
|
||||||
|
// x => 圆弧对应的圆心横坐标 x |
||||||
|
// y => 圆弧对应的圆心横坐标 y |
||||||
|
// radius => 圆弧的半径大小 |
||||||
|
// startAngle => 圆弧开始的角度,单位是弧度 |
||||||
|
// endAngle => 圆弧结束的角度,单位是弧度 |
||||||
|
// anticlockwise(可选) => 绘制方向,true 为逆时针,false 为顺时针 |
||||||
|
|
||||||
|
ctx.beginPath() |
||||||
|
// 外圆 |
||||||
|
ctx.arc(canvasW * 0.5, canvasH * 0.5, this.outsideRadius, angle, angle + baseAngle, false) |
||||||
|
// 内圆 |
||||||
|
ctx.arc(canvasW * 0.5, canvasH * 0.5, this.insideRadius, angle + baseAngle, angle, true) |
||||||
|
|
||||||
|
// 每个奖品区块背景填充颜色 |
||||||
|
if (this.colors.length === 2) { |
||||||
|
ctx.setFillStyle(this.colors[i % 2]) |
||||||
|
} else { |
||||||
|
ctx.setFillStyle(this.colors[i]) |
||||||
|
} |
||||||
|
// 填充颜色 |
||||||
|
ctx.fill() |
||||||
|
|
||||||
|
// 设置文字颜色 |
||||||
|
if (this.strFontColors.length === 1) { |
||||||
|
ctx.setFillStyle(this.strFontColors[0]) |
||||||
|
} else if (this.strFontColors.length === 2) { |
||||||
|
ctx.setFillStyle(this.strFontColors[i % 2]) |
||||||
|
} else { |
||||||
|
ctx.setFillStyle(this.strFontColors[i]) |
||||||
|
} |
||||||
|
|
||||||
|
// 开启描边 |
||||||
|
if (this.stroked) { |
||||||
|
// 设置描边颜色 |
||||||
|
ctx.setStrokeStyle(`${this.strokeColor}`) |
||||||
|
// 描边 |
||||||
|
ctx.stroke() |
||||||
|
} |
||||||
|
|
||||||
|
// 开始绘制奖品内容 |
||||||
|
// 重新映射画布上的 (0,0) 位置 |
||||||
|
let translateX = canvasW * 0.5 + Math.cos(angle + baseAngle / 2) * this.textDistance |
||||||
|
let translateY = canvasH * 0.5 + Math.sin(angle + baseAngle / 2) * this.textDistance |
||||||
|
ctx.translate(translateX, translateY) |
||||||
|
|
||||||
|
// 绘制奖品名称 |
||||||
|
let rewardName = this.strLimit(prizeItem.prizeName) |
||||||
|
|
||||||
|
// rotate方法旋转当前的绘图,因为文字是和当前扇形中心线垂直的 |
||||||
|
ctx.rotate(angle + (baseAngle / 2) + (Math.PI / 2)) |
||||||
|
|
||||||
|
// 设置文本位置并处理换行 |
||||||
|
if (this.strDirection === 'horizontal') { |
||||||
|
// 是否需要换行 |
||||||
|
if (rewardName && this.prizeNameDrawed) { |
||||||
|
let realLen = clacTextLen(rewardName).realLen |
||||||
|
let isLineBreak = realLen > this.strLineLen |
||||||
|
if (isLineBreak) { |
||||||
|
// 获得多行文本数组 |
||||||
|
let firstText = '' |
||||||
|
let lastText = '' |
||||||
|
let firstCount = 0 |
||||||
|
for (let j = 0; j < rewardName.length; j++) { |
||||||
|
firstCount += clacTextLen(rewardName[j]).byteLen |
||||||
|
if (firstCount <= (this.strLineLen * 2)) { |
||||||
|
firstText += rewardName[j] |
||||||
|
} else { |
||||||
|
lastText += rewardName[j] |
||||||
|
} |
||||||
|
} |
||||||
|
rewardName = firstText + ',' + lastText |
||||||
|
let rewardNames = rewardName.split(',') |
||||||
|
// 循环文本数组,计算每一行的文本宽度 |
||||||
|
for (let j = 0; j < rewardNames.length; j++) { |
||||||
|
if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) { |
||||||
|
// 文本的宽度信息 |
||||||
|
let tempStrSize = ctx.measureText(rewardNames[j]) |
||||||
|
let tempStrWidth = -(tempStrSize.width / 2).toFixed(2) |
||||||
|
ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple) |
||||||
|
} else { |
||||||
|
this.measureText = rewardNames[j] |
||||||
|
|
||||||
|
// 等待页面重新渲染 |
||||||
|
await this.$nextTick() |
||||||
|
|
||||||
|
let textWidth = await this.getTextWidth() |
||||||
|
let tempStrWidth = -(textWidth / 2).toFixed(2) |
||||||
|
ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple) |
||||||
|
// console.log(rewardNames[j], textWidth, j) |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (ctx.measureText && ctx.measureText(rewardName).width > 0) { |
||||||
|
// 文本的宽度信息 |
||||||
|
let tempStrSize = ctx.measureText(rewardName) |
||||||
|
let tempStrWidth = -(tempStrSize.width / 2).toFixed(2) |
||||||
|
ctx.fillText(rewardName, tempStrWidth, 0) |
||||||
|
} else { |
||||||
|
this.measureText = rewardName |
||||||
|
|
||||||
|
// 等待页面重新渲染 |
||||||
|
await this.$nextTick() |
||||||
|
|
||||||
|
let textWidth = await this.getTextWidth() |
||||||
|
let tempStrWidth = -(textWidth / 2).toFixed(2) |
||||||
|
ctx.fillText(rewardName, tempStrWidth, 0) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
let rewardNames = rewardName.split('') |
||||||
|
for (let j = 0; j < rewardNames.length; j++) { |
||||||
|
if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) { |
||||||
|
// 文本的宽度信息 |
||||||
|
let tempStrSize = ctx.measureText(rewardNames[j]) |
||||||
|
let tempStrWidth = -(tempStrSize.width / 2).toFixed(2) |
||||||
|
ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple) |
||||||
|
} else { |
||||||
|
this.measureText = rewardNames[j] |
||||||
|
|
||||||
|
// 等待页面重新渲染 |
||||||
|
await this.$nextTick() |
||||||
|
|
||||||
|
let textWidth = await this.getTextWidth() |
||||||
|
let tempStrWidth = -(textWidth / 2).toFixed(2) |
||||||
|
ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple) |
||||||
|
// console.log(rewardNames[j], textWidth, i) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// 绘制奖品图片,文字竖向展示时,不支持图片展示 |
||||||
|
if (this.imgDrawed && prizeItem.prizeImage && this.strDirection !== 'vertical') { |
||||||
|
// App-Android平台 系统 webview 更新到 Chrome84+ 后 canvas 组件绘制本地图像 uni.canvasToTempFilePath 会报错 |
||||||
|
// 统一将图片处理成 base64 |
||||||
|
// https://ask.dcloud.net.cn/question/103303 |
||||||
|
let reg = /^(https|http)/g |
||||||
|
// 处理远程图片 |
||||||
|
if (reg.test(prizeItem.prizeImage)) { |
||||||
|
let platformTips = '' |
||||||
|
// #ifdef APP-PLUS |
||||||
|
platformTips = '' |
||||||
|
// #endif |
||||||
|
// #ifdef MP |
||||||
|
platformTips = '需要处理好下载域名的白名单问题,' |
||||||
|
// #endif |
||||||
|
// #ifdef H5 |
||||||
|
platformTips = '需要处理好跨域问题,' |
||||||
|
// #endif |
||||||
|
console.warn(`###当前数据列表中的奖品图片为网络图片,${platformTips}开始尝试下载图片...###`) |
||||||
|
let res = await downloadFile(prizeItem.prizeImage) |
||||||
|
console.log('处理远程图片', res) |
||||||
|
if (res.ok) { |
||||||
|
let tempFilePath = res.tempFilePath |
||||||
|
// #ifndef MP |
||||||
|
prizeItem.prizeImage = await pathToBase64(tempFilePath) |
||||||
|
// #endif |
||||||
|
// #ifdef MP |
||||||
|
prizeItem.prizeImage = tempFilePath |
||||||
|
// #endif |
||||||
|
} else { |
||||||
|
this.handlePrizeImgSuc({ |
||||||
|
ok: false, |
||||||
|
data: res.data, |
||||||
|
msg: res.msg |
||||||
|
}) |
||||||
|
} |
||||||
|
} else { |
||||||
|
console.log('处理非远程图片', prizeItem.prizeImage) |
||||||
|
// #ifndef MP |
||||||
|
if (prizeItem.prizeImage.indexOf(';base64,') === -1) { |
||||||
|
prizeItem.prizeImage = await pathToBase64(prizeItem.prizeImage) |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
|
||||||
|
let prizeImageX = -(this.imgPxWidth * systemInfo.pixelRatio / 2) |
||||||
|
let prizeImageY = this.imgMarginPxStr * systemInfo.pixelRatio |
||||||
|
let prizeImageW = this.imgPxWidth * systemInfo.pixelRatio |
||||||
|
let prizeImageH = this.imgPxHeight * systemInfo.pixelRatio |
||||||
|
ctx.drawImage(prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH) |
||||||
|
} |
||||||
|
|
||||||
|
ctx.restore() |
||||||
|
} |
||||||
|
|
||||||
|
// 保存绘图并导出图片 |
||||||
|
ctx.draw(true, () => { |
||||||
|
let drawTimer = setTimeout(() => { |
||||||
|
clearTimeout(drawTimer) |
||||||
|
drawTimer = null |
||||||
|
|
||||||
|
// #ifdef MP-ALIPAY |
||||||
|
ctx.toTempFilePath({ |
||||||
|
destWidth: this.higtCanvasSize, |
||||||
|
destHeight: this.higtCanvasSize, |
||||||
|
success: (res) => { |
||||||
|
// console.log(res.apFilePath) |
||||||
|
this.handlePrizeImg({ |
||||||
|
ok: true, |
||||||
|
data: res.apFilePath, |
||||||
|
msg: '画布导出生成图片成功' |
||||||
|
}) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
this.handlePrizeImg({ |
||||||
|
ok: false, |
||||||
|
data: err, |
||||||
|
msg: '画布导出生成图片失败' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
// #endif |
||||||
|
|
||||||
|
// #ifndef MP-ALIPAY |
||||||
|
uni.canvasToTempFilePath({ |
||||||
|
canvasId: this.canvasId, |
||||||
|
success: (res) => { |
||||||
|
// 在 H5 平台下,tempFilePath 为 base64 |
||||||
|
// console.log(res.tempFilePath) |
||||||
|
this.handlePrizeImg({ |
||||||
|
ok: true, |
||||||
|
data: res.tempFilePath, |
||||||
|
msg: '画布导出生成图片成功' |
||||||
|
}) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
this.handlePrizeImg({ |
||||||
|
ok: false, |
||||||
|
data: err, |
||||||
|
msg: '画布导出生成图片失败' |
||||||
|
}) |
||||||
|
} |
||||||
|
}, this) |
||||||
|
// #endif |
||||||
|
}, 500) |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 处理导出的图片 |
||||||
|
handlePrizeImg(res) { |
||||||
|
if (res.ok) { |
||||||
|
let data = res.data |
||||||
|
|
||||||
|
if (!this.canvasCached) { |
||||||
|
this.lotteryImg = data |
||||||
|
this.handlePrizeImgSuc(res) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// #ifndef H5 |
||||||
|
if (this.isCacheImg) { |
||||||
|
uni.getSavedFileList({ |
||||||
|
success: (sucRes) => { |
||||||
|
let fileList = sucRes.fileList |
||||||
|
// console.log('getSavedFileList Cached', fileList) |
||||||
|
|
||||||
|
let cached = false |
||||||
|
|
||||||
|
if (fileList.length) { |
||||||
|
for (let i = 0; i < fileList.length; i++) { |
||||||
|
let item = fileList[i] |
||||||
|
if (item.filePath === data) { |
||||||
|
cached = true |
||||||
|
this.lotteryImg = data |
||||||
|
|
||||||
|
console.info('经查,本地缓存中存在的转盘图可用,本次将不再绘制转盘') |
||||||
|
this.handlePrizeImgSuc(res) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!cached) { |
||||||
|
console.info('经查,本地缓存中存在的转盘图不可用,需要重新初始化转盘绘制') |
||||||
|
this.initCanvasDraw() |
||||||
|
} |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
this.initCanvasDraw() |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
uni.saveFile({ |
||||||
|
tempFilePath: data, |
||||||
|
success: (sucRes) => { |
||||||
|
let filePath = sucRes.savedFilePath |
||||||
|
// console.log('saveFile', filePath) |
||||||
|
setStore(`${this.canvasId}LotteryImg`, filePath) |
||||||
|
this.lotteryImg = filePath |
||||||
|
this.handlePrizeImgSuc({ |
||||||
|
ok: true, |
||||||
|
data: filePath, |
||||||
|
msg: '画布导出生成图片成功' |
||||||
|
}) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
this.handlePrizeImg({ |
||||||
|
ok: false, |
||||||
|
data: err, |
||||||
|
msg: '画布导出生成图片失败' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
// #endif |
||||||
|
// #ifdef H5 |
||||||
|
setStore(`${this.canvasId}LotteryImg`, data) |
||||||
|
this.lotteryImg = data |
||||||
|
this.handlePrizeImgSuc(res) |
||||||
|
|
||||||
|
// console info |
||||||
|
let consoleText = this.isCacheImg ? '缓存' : '导出' |
||||||
|
console.info(`当前为 H5 端,使用${consoleText}中的 base64 图`) |
||||||
|
// #endif |
||||||
|
} else { |
||||||
|
console.error('处理导出的图片失败', res) |
||||||
|
uni.hideLoading() |
||||||
|
// #ifdef H5 |
||||||
|
console.error('###当前为 H5 端,下载网络图片需要后端配置允许跨域###') |
||||||
|
// #endif |
||||||
|
// #ifdef MP |
||||||
|
console.error('###当前为小程序端,下载网络图片需要配置域名白名单###') |
||||||
|
// #endif |
||||||
|
} |
||||||
|
}, |
||||||
|
// 处理图片完成 |
||||||
|
handlePrizeImgSuc (res) { |
||||||
|
uni.hideLoading() |
||||||
|
this.$emit('finish', { |
||||||
|
ok: res.ok, |
||||||
|
data: res.data, |
||||||
|
msg: res.ok ? this.successMsg : this.failMsg |
||||||
|
}) |
||||||
|
this.almostLotteryTip = res.ok ? this.successMsg : this.failMsg |
||||||
|
}, |
||||||
|
// 兼容 app 端不支持 ctx.measureText |
||||||
|
// 已知问题:初始绘制时,低端安卓机 平均耗时 2s |
||||||
|
// hbx 2.8.12+ 已在 app 端支持 |
||||||
|
getTextWidth() { |
||||||
|
console.warn('正在采用兼容方式获取文本的 size 信息,虽然没有任何问题,如果可以,请将此 systemInfo 及 hbx 版本号 反馈给作者', systemInfo) |
||||||
|
let query = uni.createSelectorQuery().in(this) |
||||||
|
let nodesRef = query.select('.almost-lottery__measureText') |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
nodesRef.fields({ |
||||||
|
size: true, |
||||||
|
}, (res) => { |
||||||
|
resolve(res.width) |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 处理文字溢出 |
||||||
|
strLimit(value) { |
||||||
|
let maxLength = this.strMaxLen |
||||||
|
if (!value || !maxLength) return value |
||||||
|
return clacTextLen(value).realLen > maxLength ? value.slice(0, maxLength - 1) + '..' : value |
||||||
|
}, |
||||||
|
// 检查本地缓存中是否存在转盘图 |
||||||
|
checkCacheImg () { |
||||||
|
console.log('检查本地缓存中是否存在转盘图') |
||||||
|
// 检查是否已有缓存的转盘图 |
||||||
|
// 检查是否与本次奖品数据相同 |
||||||
|
this.oldLotteryImg = getStore(`${this.canvasId}LotteryImg`) |
||||||
|
let oldPrizeList = getStore(`${this.canvasId}PrizeList`) |
||||||
|
let newPrizeList = JSON.stringify(this.prizeList) |
||||||
|
if (this.oldLotteryImg) { |
||||||
|
if (oldPrizeList === newPrizeList) { |
||||||
|
console.log(`经查,本地缓存中存在转盘图 => ${this.oldLotteryImg}`) |
||||||
|
this.isCacheImg = true |
||||||
|
|
||||||
|
console.log('需要继续判断这张缓存图是否可用') |
||||||
|
this.handlePrizeImg({ |
||||||
|
ok: true, |
||||||
|
data: this.oldLotteryImg, |
||||||
|
msg: '画布导出生成图片成功' |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
console.log('经查,本地缓存中不存在转盘图') |
||||||
|
this.initCanvasDraw() |
||||||
|
}, |
||||||
|
// 初始化绘制 |
||||||
|
initCanvasDraw () { |
||||||
|
console.log('开始初始化转盘绘制') |
||||||
|
this.isCacheImg = false |
||||||
|
this.lotteryImg = '' |
||||||
|
clearStore(`${this.canvasId}LotteryImg`) |
||||||
|
setStore(`${this.canvasId}PrizeList`, this.prizeList) |
||||||
|
this.onCreateCanvas() |
||||||
|
}, |
||||||
|
// 预处理初始化 |
||||||
|
async beforeInit () { |
||||||
|
// 处理 rpx 自适应尺寸 |
||||||
|
let lotterySize = await new Promise((resolve) => { |
||||||
|
uni.createSelectorQuery().in(this).select('.almost-lottery__wrap').boundingClientRect((rects) => { |
||||||
|
resolve(rects) |
||||||
|
// console.log('处理 lottery rpx 的自适应', rects) |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
let actionSize = await new Promise((resolve) => { |
||||||
|
uni.createSelectorQuery().in(this).select('.lottery-action').boundingClientRect((rects) => { |
||||||
|
resolve(rects) |
||||||
|
// console.log('处理 action rpx 的自适应', rects) |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
let strMarginSize = await new Promise((resolve) => { |
||||||
|
uni.createSelectorQuery().in(this).select('.str-margin-outside').boundingClientRect((rects) => { |
||||||
|
resolve(rects) |
||||||
|
// console.log('处理 str-margin-outside rpx 的自适应', rects) |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
let imgMarginStr = await new Promise((resolve) => { |
||||||
|
uni.createSelectorQuery().in(this).select('.img-margin-str').boundingClientRect((rects) => { |
||||||
|
resolve(rects) |
||||||
|
// console.log('处理 img-margin-str rpx 的自适应', rects) |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
let imgSize = await new Promise((resolve) => { |
||||||
|
uni.createSelectorQuery().in(this).select('.img-size').boundingClientRect((rects) => { |
||||||
|
resolve(rects) |
||||||
|
// console.log('处理 img-size rpx 的自适应', rects) |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
this.lotteryPxSize = Math.floor(lotterySize.width) |
||||||
|
this.actionPxSize = Math.floor(actionSize.width) |
||||||
|
this.canvasPxSize = this.lotteryPxSize - Math.floor(actionSize.left) + Math.floor(lotterySize.left) |
||||||
|
this.strMarginPxOutside = Math.floor(strMarginSize.left) - Math.floor(lotterySize.left) |
||||||
|
this.imgMarginPxStr = Math.floor(imgMarginStr.left) - Math.floor(lotterySize.left) |
||||||
|
this.imgPxWidth = Math.floor(imgSize.width) |
||||||
|
this.imgPxHeight = Math.floor(imgSize.height) |
||||||
|
|
||||||
|
let stoTimer = setTimeout(() => { |
||||||
|
clearTimeout(stoTimer) |
||||||
|
stoTimer = null |
||||||
|
|
||||||
|
// 判断画板是否设置缓存 |
||||||
|
if (this.canvasCached) { |
||||||
|
this.checkCacheImg() |
||||||
|
} else { |
||||||
|
this.initCanvasDraw() |
||||||
|
} |
||||||
|
this.transitionDuration = this.duration |
||||||
|
}, 50) |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.$nextTick(() => { |
||||||
|
let stoTimer = setTimeout(() => { |
||||||
|
clearTimeout(stoTimer) |
||||||
|
stoTimer = null |
||||||
|
|
||||||
|
this.beforeInit() |
||||||
|
}, 50) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.almost-lottery { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__wrap { |
||||||
|
position: relative; |
||||||
|
// background-color: red; |
||||||
|
} |
||||||
|
.lottery-action, |
||||||
|
.str-margin-outside, |
||||||
|
.img-margin-str, |
||||||
|
.img-size { |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
z-index: -1; |
||||||
|
// background-color: blue; |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__wrap { |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__action, |
||||||
|
.almost-lottery__bg, |
||||||
|
.almost-lottery__canvas { |
||||||
|
position: absolute; |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__canvas { |
||||||
|
left: -9999px; |
||||||
|
opacity: 0; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__canvas-img, |
||||||
|
.almost-lottery__action-bg { |
||||||
|
display: block; |
||||||
|
transition: transform cubic-bezier(.34, .12, .05, .95); |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__tip { |
||||||
|
color: #FFFFFF; |
||||||
|
font-size: 24rpx; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.almost-lottery__measureText { |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
white-space: nowrap; |
||||||
|
font-size: 12px; |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,78 @@ |
|||||||
|
{ |
||||||
|
"id": "almost-lottery", |
||||||
|
"displayName": "Almost-Lottery抽奖转盘", |
||||||
|
"version": "1.8.5", |
||||||
|
"description": "【荣获2021插件大赛三等奖】提供奇数、缓存等众多配置项,更有抽奖概率、抽奖次数、付费抽奖等功能内置于示例项目中,完美支持APP、各平台小程序、H5、PC,同时提供 uniCloud 云端版本", |
||||||
|
"keywords": [ |
||||||
|
"转盘", |
||||||
|
"抽奖", |
||||||
|
"大转盘抽奖" |
||||||
|
], |
||||||
|
"repository": "https://github.com/ialmost/almost-components_uniapp", |
||||||
|
"engines": { |
||||||
|
"HBuilderX": "^3.1.22" |
||||||
|
}, |
||||||
|
"dcloudext": { |
||||||
|
"category": [ |
||||||
|
"前端组件", |
||||||
|
"通用组件" |
||||||
|
], |
||||||
|
"sale": { |
||||||
|
"regular": { |
||||||
|
"price": "0.00" |
||||||
|
}, |
||||||
|
"sourcecode": { |
||||||
|
"price": "0.00" |
||||||
|
} |
||||||
|
}, |
||||||
|
"contact": { |
||||||
|
"qq": "" |
||||||
|
}, |
||||||
|
"declaration": { |
||||||
|
"ads": "无", |
||||||
|
"data": "无", |
||||||
|
"permissions": "无" |
||||||
|
}, |
||||||
|
"npmurl": "" |
||||||
|
}, |
||||||
|
"uni_modules": { |
||||||
|
"dependencies": [], |
||||||
|
"encrypt": [], |
||||||
|
"platforms": { |
||||||
|
"cloud": { |
||||||
|
"tcb": "y", |
||||||
|
"aliyun": "y" |
||||||
|
}, |
||||||
|
"client": { |
||||||
|
"App": { |
||||||
|
"app-vue": "y", |
||||||
|
"app-nvue": "n" |
||||||
|
}, |
||||||
|
"H5-mobile": { |
||||||
|
"Safari": "y", |
||||||
|
"Android Browser": "y", |
||||||
|
"微信浏览器(Android)": "y", |
||||||
|
"QQ浏览器(Android)": "y" |
||||||
|
}, |
||||||
|
"H5-pc": { |
||||||
|
"Chrome": "y", |
||||||
|
"IE": "y", |
||||||
|
"Edge": "y", |
||||||
|
"Firefox": "y", |
||||||
|
"Safari": "y" |
||||||
|
}, |
||||||
|
"小程序": { |
||||||
|
"微信": "y", |
||||||
|
"阿里": "y", |
||||||
|
"百度": "y", |
||||||
|
"字节跳动": "y", |
||||||
|
"QQ": "y" |
||||||
|
}, |
||||||
|
"快应用": { |
||||||
|
"华为": "u", |
||||||
|
"联盟": "u" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,159 @@ |
|||||||
|
# almost-lottery |
||||||
|
*使用 Canvas 绘制的抽奖转盘,提供奇数、缓存等众多配置项,更有抽奖概率、抽奖次数、付费抽奖等功能内置于示例项目中* |
||||||
|
|
||||||
|
|
||||||
|
> <br /> |
||||||
|
> |
||||||
|
> 如果用着还行,请支持一下 |
||||||
|
> - 前往 [GitHub](https://github.com/ialmost/almost-components_uniapp) 给个 Star |
||||||
|
> - 前往 [UniApp](https://ext.dcloud.net.cn/plugin?id=1030) 给个五星 |
||||||
|
> - 使用中遇到问题时,可以添加 **QQ群 20441313** |
||||||
|
> |
||||||
|
> <br /> |
||||||
|
|
||||||
|
|
||||||
|
## 基于 uniCloud 开发的云端 Almost-Lottery 抽奖转盘,欢迎尝试体验 |
||||||
|
- [Almost-Lottery抽奖转盘的云端一体页面](https://ext.dcloud.net.cn/plugin?id=5763) |
||||||
|
- [Almost-Lottery抽奖转盘的配置中心](https://ext.dcloud.net.cn/plugin?id=5762) |
||||||
|
|
||||||
|
|
||||||
|
## 高能预警 |
||||||
|
- 本插件仅支持 `uni_modules` 模式,强烈推荐使用该模式,**非 `uni_modules` 模式不再维护** |
||||||
|
- 在使用本插件之前,强烈建议使用 `HBuilderX` 导入示例项目验证可用性并参照修改 |
||||||
|
|
||||||
|
## 功能概要 |
||||||
|
- [x] 可配置奖品文字 **支持横向/竖向展示** |
||||||
|
- [x] 可配置每个奖品区块的背景颜色 |
||||||
|
- [x] 可配置每个奖品区块的奖品文字颜色 |
||||||
|
- [x] 可配置奖品区块是否开启描边以及边框的颜色,默认不开启 |
||||||
|
- [x] 可配置转盘外环和抽奖按钮图 |
||||||
|
- [x] 可配置每个奖品区块的奖品图片,**当图片是网络地址时,小程序端需要配置白名单,H5端需要允许跨域,奖品文字为竖向时不支持展示奖品图片** |
||||||
|
- [x] 奖品列表支持奇数,**奇数时需尽量能被 `360` 除尽** |
||||||
|
- [x] 可配置内圈与外圈的间距 |
||||||
|
- [x] 可配置轮盘旋转或指针旋转 |
||||||
|
- [x] 可配置画板是否缓存,默认不开启 |
||||||
|
- [x] 更多配置请查看API说明 |
||||||
|
|
||||||
|
## 示例项目附加功能 |
||||||
|
- [x] 中奖概率,**强烈推荐中奖概率应由后端控制** |
||||||
|
- [x] 抽奖次数 |
||||||
|
- [x] 付费抽奖 |
||||||
|
|
||||||
|
|
||||||
|
## 注意事项 |
||||||
|
|
||||||
|
- 编译到小程序端时,请务必勾选ES6转ES5 |
||||||
|
|
||||||
|
- `@reset-index="prizeIndex = -1"` 必须默认写入到 `template` 中,不可删除 |
||||||
|
|
||||||
|
- 每个奖品区块的奖品图片尺寸不宜过大,图片越大,绘制的过程越慢,尽量将图片尺寸控制在 `100*100` 以内,且图片大小控制在 `40KB` 以内 |
||||||
|
|
||||||
|
- 关于中奖概率的配置,请下载示例项目,参照 `pages/index/index.vue` 中的代码进行配置 |
||||||
|
|
||||||
|
- 组件本身不涉及任何业务逻辑,与业务相关的代码建议都放在 `pages/index/index.vue` 中 |
||||||
|
|
||||||
|
|
||||||
|
## 代码演示 |
||||||
|
#### 基础用法 |
||||||
|
``` |
||||||
|
// template |
||||||
|
// @reset-index="prizeIndex = -1" 必须默认写入到 template 中,不可删除 |
||||||
|
<almost-lottery |
||||||
|
:prizeList="prizeList" |
||||||
|
:prizeIndex="prizeIndex" |
||||||
|
@reset-index="prizeIndex = -1" |
||||||
|
@draw-start="handleDrawStart" |
||||||
|
@draw-end="handleDrawEnd" |
||||||
|
@finish="handleDrawFinish" |
||||||
|
v-if="prizeList.length" |
||||||
|
/> |
||||||
|
|
||||||
|
// script |
||||||
|
import AlmostLottery from '@/uni_modules/almost-lottery/components/almost-lottery/almost-lottery.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
AlmostLottery |
||||||
|
}, |
||||||
|
data () { |
||||||
|
return { |
||||||
|
// 以下是奖品配置数据 |
||||||
|
// 奖品数据 |
||||||
|
prizeList: [], |
||||||
|
// 中奖下标 |
||||||
|
prizeIndex: -1 |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 本次抽奖开始 |
||||||
|
handleDrawStart () { |
||||||
|
// 这里需要处理你的中奖逻辑,并得出 prizeIndex |
||||||
|
// 请查看示例项目中的代码 |
||||||
|
}, |
||||||
|
// 本次抽奖结束 |
||||||
|
handleDrawEnd () { |
||||||
|
// 完成抽奖后,这里处理你拿到结果后的逻辑 |
||||||
|
// 请查看示例项目中的代码 |
||||||
|
}, |
||||||
|
// 抽奖转盘绘制完成 |
||||||
|
handleDrawFinish (res) { |
||||||
|
// 抽奖转盘准备就绪后,这里处理你的逻辑 |
||||||
|
// 请查看示例项目中的代码 |
||||||
|
// console.log('抽奖转盘绘制完成', res) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## API |
||||||
|
#### Props |
||||||
|
参数 | 说明 | 类型 | 默认值 |
||||||
|
:---|:---|:---|:--- |
||||||
|
pixelRatio | 移动端设计稿的像素比基准值,**涉及到 `rpx` 的适配问题** | *`Number`* | `2` |
||||||
|
canvasId | Canvas的标识,**多画板情况下需要配置不同的标识** | *`String`* | `'almostLottery'` |
||||||
|
lotterySize | 抽奖转盘的整体尺寸,单位 `rpx` | *`Number`* | `600` |
||||||
|
actionSize | 抽奖按钮的尺寸,单位 `rpx` | *`Number`* | `200` |
||||||
|
canvasMarginOutside | Canvas边缘距离转盘边缘的距离,单位`rpx` | *`Number`* | `90` |
||||||
|
prizeIndex | 获奖奖品在奖品列表中的序号,**每次抽奖结束后会自动重置为 `-1`** | *`Number`* | `-1` |
||||||
|
prizeList | 奖品列表,支持奇数(尽量能被 `360` 除尽),**为奇数时需要重设 `colors` 参数** | *`Array`* | - |
||||||
|
lotteryBg | 转盘外环图片 | `String` | `默认内置的本地图片` |
||||||
|
actionBg | 抽奖按钮图片 | `String` | `默认内置的本地图片` |
||||||
|
colors | 奖品区块对应的背景颜色,默认 2 个颜色相互交替,**也可以对每个区块设置不同颜色** | *`Array`* | `['#FFFFFF', '#FFBF05']` |
||||||
|
prizeNameDrawed | 是否绘制奖品名称 | *`Boolean`* | `true` |
||||||
|
stroked | 是否开启奖品区块描边 | *`Boolean`* | `false` |
||||||
|
strDirection | 奖品名称展示方向,可选值 `'horizontal'` => 横向 `'vertical'` => 竖向 | *`String`* | `'horizontal'` |
||||||
|
strokeColor | 奖品区块边框颜色 | *`String`* | `'#FFBF05'` |
||||||
|
rotateType | 旋转的类型,可选值 `'roulette'` => 轮盘旋转 `'pointer'` => 指针旋转 | *`String`* | `'roulette'` |
||||||
|
duration | 转盘旋转的动画时长,单位:秒 | *`Number`* | `8` |
||||||
|
ringCount | 旋转的圈数 | *`Number`* | `8` |
||||||
|
pointerPosition | 点击抽奖按钮指针的位置,可选值 `'edge'` => 指向边界 `'middle'` => 指向中间 | *`String`* | `'edge'` |
||||||
|
strFontColors | 奖品文字颜色,默认 2 个颜色相互交替,**也可以对每个区块的文字设置不同颜色,或仅设置一个颜色** | *`Array`* | `['#FFBF05', '#FFFFFF']` |
||||||
|
strFontSize | 奖品名称的字号,单位 `rpx` | *`Number`* | `24` |
||||||
|
strLineHeight | 奖品名称多行情况下的行高 | *`Number`* | `1.2` |
||||||
|
strMaxLen | 奖品名称长度限制,**文字竖向时不生效** | *`Number`* | `12` |
||||||
|
strLineLen | 奖品名称在多行情况下第一行文字的长度,**文字竖向时不生效** | *`Number`* | `6` |
||||||
|
strMarginOutside | 奖品文字相对轮盘边缘的距离,单位 `rpx` | *`Number`* | `strFontSize 的一半` |
||||||
|
imgMarginStr | 奖品图片相对奖品文字的距离,单位 `rpx` | *`Number`* | `60` |
||||||
|
imgWidth | 奖品图片的宽度,单位 `rpx` | *`Number`* | `50` |
||||||
|
imgHeight | 奖品图片的高度,单位 `rpx` | *`Number`* | `50` |
||||||
|
imgDrawed | 是否绘制奖品图片,默认绘制 | *`Boolean`* | `true` |
||||||
|
successMsg | 转盘绘制成功的提示 | *`String`* | `'奖品准备就绪,快来参与抽奖吧'` |
||||||
|
failMsg | 转盘绘制失败的提示 | *`String`* | `'奖品仍在准备中,请稍后再来...'` |
||||||
|
canvasCached | 是否开启缓存,避免在数据不变的情况下重复绘制,建议在生产环境中开启 | *`Boolean`* | `false` |
||||||
|
|
||||||
|
#### Events |
||||||
|
事件名 | 说明 | 回调参数 |
||||||
|
:---|:---|:--- |
||||||
|
@reset-index | 每次抽奖结束后重置获奖的序号为 `-1`,**该事件必须默认写入到 `template` 中,不可删除** | - |
||||||
|
@draw-start | 转盘旋转开始时触发 | - |
||||||
|
@draw-end | 转盘旋转结束时触发 | - |
||||||
|
@finish | Canvas转盘绘制完成时触发 | `{ ok: 绘制是否成功, data: 转盘的图片, msg: 绘制结果的提示 }` |
||||||
|
|
||||||
|
#### prizeList 数据结构 |
||||||
|
*请按如下数据字段对你对奖品列表数据结构进行调整* |
||||||
|
键名 | 说明 | 类型 |
||||||
|
:---|:---|:--- |
||||||
|
prizeId | 奖品对应 `ID` | *`Number`* |
||||||
|
prizeName | 奖品名称 | *`String`* |
||||||
|
prizeStock | 奖品库存 | *`Number`* |
||||||
|
prizeWeight | 奖品权重 | *`Number`* |
||||||
|
prizeImage | 奖品图片地址,网络图片仅支持`http`和`https`协议 | *`String`* |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,303 @@ |
|||||||
|
/** |
||||||
|
* 存储 localStorage 数据 |
||||||
|
* @param {String} name - 缓存数据的标识 |
||||||
|
* @param {any} content - 缓存的数据内容 |
||||||
|
*/ |
||||||
|
export const setStore = (name, content) => { |
||||||
|
if (!name) return |
||||||
|
if (typeof content !== 'string') { |
||||||
|
content = JSON.stringify(content) |
||||||
|
} |
||||||
|
uni.setStorageSync(name, content) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取 localStorage 数据 |
||||||
|
* @param {String} name - 缓存数据的标识 |
||||||
|
*/ |
||||||
|
export const getStore = (name) => { |
||||||
|
if (!name) return |
||||||
|
return uni.getStorageSync(name) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 清除 localStorage 数据 |
||||||
|
* @param {String} name - 缓存数据的标识 |
||||||
|
*/ |
||||||
|
export const clearStore = (name) => { |
||||||
|
if (name) { |
||||||
|
uni.removeStorageSync(name) |
||||||
|
} else { |
||||||
|
console.log('清理本地全部缓存') |
||||||
|
uni.clearStorageSync() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 计算文本的长度 |
||||||
|
* @param {String} text - 文本内容 |
||||||
|
*/ |
||||||
|
export const clacTextLen = (text) => { |
||||||
|
if (!text) return { byteLen: 0, realLen: 0 } |
||||||
|
text += '' |
||||||
|
let clacLen = 0 |
||||||
|
for (let i = 0; i < text.length; i++) { |
||||||
|
if ((text.charCodeAt(i) < 0) || (text.charCodeAt(i) > 255)) { |
||||||
|
clacLen += 2 |
||||||
|
} else { |
||||||
|
clacLen += 1 |
||||||
|
} |
||||||
|
} |
||||||
|
// console.log(`当前文本 ${text} 的长度为 ${clacLen / 2}`)
|
||||||
|
return { |
||||||
|
byteLen: clacLen, |
||||||
|
realLen: clacLen / 2 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 下载文件,并返回临时路径 |
||||||
|
* @return {String} 临时路径 |
||||||
|
* @param {String} fileUrl - 网络地址 |
||||||
|
*/ |
||||||
|
export const downloadFile = (fileUrl) => { |
||||||
|
return new Promise((resolve) => { |
||||||
|
uni.downloadFile({ |
||||||
|
url: fileUrl, |
||||||
|
success: (res) => { |
||||||
|
// #ifdef MP-ALIPAY
|
||||||
|
if (res.errMsg === 'downloadFile:ok') { |
||||||
|
resolve({ |
||||||
|
ok: true, |
||||||
|
data: res.errMsg, |
||||||
|
tempFilePath: res.tempFilePath |
||||||
|
}) |
||||||
|
} else { |
||||||
|
resolve({ |
||||||
|
ok: false, |
||||||
|
data: res.errMsg, |
||||||
|
msg: '图片下载失败' |
||||||
|
}) |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
// #ifndef MP-ALIPAY
|
||||||
|
if (res.statusCode === 200) { |
||||||
|
resolve({ |
||||||
|
ok: true, |
||||||
|
data: res.errMsg, |
||||||
|
tempFilePath: res.tempFilePath |
||||||
|
}) |
||||||
|
} else { |
||||||
|
resolve({ |
||||||
|
ok: false, |
||||||
|
data: res.errMsg, |
||||||
|
msg: '图片下载失败' |
||||||
|
}) |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
resolve({ |
||||||
|
ok: false, |
||||||
|
data: err.errMsg, |
||||||
|
msg: '图片下载失败' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 清理应用已缓存的文件 |
||||||
|
*/ |
||||||
|
export const clearCacheFile = () => { |
||||||
|
// #ifndef H5
|
||||||
|
uni.getSavedFileList({ |
||||||
|
success: (res) => { |
||||||
|
let fileList = res.fileList |
||||||
|
if (fileList.length) { |
||||||
|
for (let i = 0; i < fileList.length; i++) { |
||||||
|
uni.removeSavedFile({ |
||||||
|
filePath: fileList[i].filePath, |
||||||
|
complete: () => { |
||||||
|
console.log('清除缓存已完成') |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
console.log('getSavedFileList Fail') |
||||||
|
} |
||||||
|
}) |
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
clearStore() |
||||||
|
// #endif
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 图像转换工具,可用于图像和base64的转换
|
||||||
|
// https://ext.dcloud.net.cn/plugin?id=123
|
||||||
|
const getLocalFilePath = (path) => { |
||||||
|
if ( |
||||||
|
path.indexOf('_www') === 0 || |
||||||
|
path.indexOf('_doc') === 0 || |
||||||
|
path.indexOf('_documents') === 0 || |
||||||
|
path.indexOf('_downloads') === 0 |
||||||
|
) return path |
||||||
|
|
||||||
|
if (path.indexOf('/storage/emulated/0/') === 0) return path |
||||||
|
|
||||||
|
if (path.indexOf('/storage/sdcard0/') === 0) return path |
||||||
|
|
||||||
|
if (path.indexOf('/var/mobile/') === 0) return path |
||||||
|
|
||||||
|
if (path.indexOf('file://') === 0) return path |
||||||
|
|
||||||
|
if (path.indexOf('/') === 0) { |
||||||
|
// ios 无法获取本地路径
|
||||||
|
let localFilePath = plus.os.name === 'iOS' ? path : plus.io.convertLocalFileSystemURL(path) |
||||||
|
if (localFilePath !== path) { |
||||||
|
return localFilePath |
||||||
|
} else { |
||||||
|
path = path.substring(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return '_www/' + path |
||||||
|
} |
||||||
|
|
||||||
|
export const pathToBase64 = (path) => { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (typeof window === 'object' && 'document' in window) { |
||||||
|
if (typeof FileReader === 'function') { |
||||||
|
let xhr = new XMLHttpRequest() |
||||||
|
xhr.open('GET', path, true) |
||||||
|
xhr.responseType = 'blob' |
||||||
|
xhr.onload = function() { |
||||||
|
if (this.status === 200) { |
||||||
|
let fileReader = new FileReader() |
||||||
|
fileReader.onload = function(e) { |
||||||
|
resolve(e.target.result) |
||||||
|
} |
||||||
|
fileReader.onerror = reject |
||||||
|
fileReader.readAsDataURL(this.response) |
||||||
|
} |
||||||
|
} |
||||||
|
xhr.onerror = reject |
||||||
|
xhr.send() |
||||||
|
return |
||||||
|
} |
||||||
|
let canvas = document.createElement('canvas') |
||||||
|
let c2x = canvas.getContext('2d') |
||||||
|
let img = new Image |
||||||
|
img.onload = function() { |
||||||
|
canvas.width = img.width |
||||||
|
canvas.height = img.height |
||||||
|
c2x.drawImage(img, 0, 0) |
||||||
|
resolve(canvas.toDataURL()) |
||||||
|
canvas.height = canvas.width = 0 |
||||||
|
} |
||||||
|
img.onerror = reject |
||||||
|
img.src = path |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof plus === 'object') { |
||||||
|
let tempPath = getLocalFilePath(path) |
||||||
|
plus.io.resolveLocalFileSystemURL(tempPath, (entry) => { |
||||||
|
entry.file((file) => { |
||||||
|
let fileReader = new plus.io.FileReader() |
||||||
|
fileReader.onload = function(data) { |
||||||
|
resolve(data.target.result) |
||||||
|
} |
||||||
|
fileReader.onerror = function(error) { |
||||||
|
console.log(error) |
||||||
|
reject(error) |
||||||
|
} |
||||||
|
fileReader.readAsDataURL(file) |
||||||
|
}, (error) => { |
||||||
|
reject(error) |
||||||
|
}) |
||||||
|
}, (error) => { |
||||||
|
reject(error) |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) { |
||||||
|
wx.getFileSystemManager().readFile({ |
||||||
|
filePath: path, |
||||||
|
encoding: 'base64', |
||||||
|
success: (res) => { |
||||||
|
resolve('data:image/png;base64,' + res.data) |
||||||
|
}, |
||||||
|
fail: (error) => { |
||||||
|
reject(error) |
||||||
|
} |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
reject(new Error('not support')) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const base64ToPath = (base64) => { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (typeof window === 'object' && 'document' in window) { |
||||||
|
base64 = base64.split(',') |
||||||
|
let type = base64[0].match(/:(.*?);/)[1] |
||||||
|
let str = atob(base64[1]) |
||||||
|
let n = str.length |
||||||
|
let array = new Uint8Array(n) |
||||||
|
while (n--) { |
||||||
|
array[n] = str.charCodeAt(n) |
||||||
|
} |
||||||
|
return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], { |
||||||
|
type: type |
||||||
|
}))) |
||||||
|
} |
||||||
|
let extName = base64.match(/data\:\S+\/(\S+);/) |
||||||
|
if (extName) { |
||||||
|
extName = extName[1] |
||||||
|
} else { |
||||||
|
reject(new Error('base64 error')) |
||||||
|
} |
||||||
|
let fileName = Date.now() + '.' + extName |
||||||
|
if (typeof plus === 'object') { |
||||||
|
let bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now()) |
||||||
|
bitmap.loadBase64Data(base64, () => { |
||||||
|
let filePath = '_doc/uniapp_temp/' + fileName |
||||||
|
bitmap.save(filePath, {}, () => { |
||||||
|
bitmap.clear() |
||||||
|
resolve(filePath) |
||||||
|
}, (error) => { |
||||||
|
bitmap.clear() |
||||||
|
reject(error) |
||||||
|
}) |
||||||
|
}, (error) => { |
||||||
|
bitmap.clear() |
||||||
|
reject(error) |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) { |
||||||
|
let filePath = wx.env.USER_DATA_PATH + '/' + fileName |
||||||
|
wx.getFileSystemManager().writeFile({ |
||||||
|
filePath: filePath, |
||||||
|
data: base64.replace(/^data:\S+\/\S+;base64,/, ''), |
||||||
|
encoding: 'base64', |
||||||
|
success: () => { |
||||||
|
resolve(filePath) |
||||||
|
}, |
||||||
|
fail: (error) => { |
||||||
|
reject(error) |
||||||
|
} |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
reject(new Error('not support')) |
||||||
|
}) |
||||||
|
} |