index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. <template>
  2. <view class="main" @click="end" >
  3. <!-- <view class="title-container">
  4. {{name}}
  5. </view> -->
  6. <view v-if="options.length>0" class="canvas-container">
  7. <canvas canvas-id="canvas" id="canvas" :style="canvasStyle" />
  8. </view>
  9. <!-- <image src="./national-2024-1.png"
  10. style="width: 560rpx; height: 560rpx;z-index: 999; margin-top: -560rpx;"
  11. ></image> -->
  12. <image src="./national-2024-7.png"
  13. style="width: 660rpx; height: 640rpx;z-index: 999; margin-top: -600rpx;"
  14. ></image>
  15. <image src="./national-2024-6.png"
  16. style="width: 660rpx; height: 500rpx; margin-top: -160rpx;"
  17. ></image>
  18. <view class="show1" style=" position: relative;
  19. top: -280rpx;
  20. color: rgba(157, 58, 15, 1);
  21. font-size: 56rpx;" >
  22. {{name}}
  23. </view>
  24. <!-- <view v-if="showBtn" btn-container>
  25. <button type="primary" :disabled="isLottery" @click="playReward">{{btnTitle}}</button>
  26. </view> -->
  27. </view>
  28. </template>
  29. <script>
  30. var img2=require('./national-2024-2.png')
  31. var img3=require('./national-2024-3.png')
  32. var ctx = null;
  33. export default {
  34. props: {
  35. // 弹窗内容 ,当 turnModalContent=【】时,默认弹窗内容为抽奖结果,并且不展示标题
  36. turnModalContent: {
  37. type: Array,
  38. default () {
  39. return []
  40. }
  41. },
  42. // 减速,值越小,减速效果越明显 turnReduceSpeed
  43. turnReduceSpeed: {
  44. type: Number,
  45. default: 50
  46. },
  47. // 表示转速:表示再转多少圈再进行抽奖
  48. turnCircle: {
  49. type: Number,
  50. default: 0
  51. },
  52. // 转盘名称
  53. name: {
  54. type: String,
  55. default: "摇奖中..."
  56. },
  57. // 画布宽度
  58. width: {
  59. type: Number,
  60. default: 100,
  61. },
  62. // 画布高度
  63. height: {
  64. type: Number,
  65. default: 100
  66. },
  67. // 画布内字体大小
  68. fontSize: {
  69. type: Number,
  70. default: 18
  71. },
  72. // 钩子函数,抽奖开始前执行,返回值为选项列表下标,若返回值为-1,则进行随机抽奖
  73. setWinnerFn: {
  74. type: Function,
  75. default: null,
  76. },
  77. // // 钩子函数,抽奖开始前执行
  78. // beforePlay: {
  79. // type: Function,
  80. // default: null,
  81. // },
  82. // // 钩子函数,抽奖结束后执行
  83. // afterPlay: {
  84. // type: Function,
  85. // default: null,
  86. // },
  87. // 如果 true,则 使用组件默认的选项数据
  88. isUseDefaultOptions: {
  89. type: Boolean,
  90. default: false
  91. },
  92. // 选项列表
  93. data: {
  94. type: Array,
  95. default: []
  96. },
  97. // 是否要展示开始按钮
  98. showBtn: {
  99. type: Boolean,
  100. default: false,
  101. },
  102. // 按钮文本
  103. btnTitle: {
  104. type: String,
  105. default: "开始"
  106. }
  107. },
  108. data() {
  109. return {
  110. img2:img2,
  111. img3:img3,
  112. options: [{
  113. id: 0, // 唯一id
  114. name: '水饺', // 名称
  115. // "weight": 50, // 中奖权重,0-100
  116. img: '', // 展示图片
  117. color: "#f6e174" // 轮盘区域底色
  118. },
  119. {
  120. id: 2,
  121. name: '火锅',
  122. img: '',
  123. color: "#94494d"
  124. },
  125. {
  126. id: 3,
  127. name: '川菜',
  128. img: '',
  129. color: "#ffaa7f"
  130. },
  131. {
  132. id: 4,
  133. name: '麻辣烫',
  134. img: '',
  135. color: "#a48342"
  136. },
  137. {
  138. id: 5,
  139. name: '炸鸡汉堡',
  140. img: '',
  141. color: "#a25f81"
  142. }
  143. ],
  144. isLottery: false, // 是否正在抽奖
  145. };
  146. },
  147. computed: {
  148. canvasStyle() {
  149. return "width:" + this.width + "rpx; height:" + this.height + "rpx;";
  150. },
  151. },
  152. methods: {
  153. end(){
  154. if (this.isLottery) {
  155. uni.showToast({
  156. title: "摇奖中...",
  157. icon: "none"
  158. })
  159. }else{
  160. this.$emit("end")
  161. }
  162. },
  163. async playReward() {
  164. if (this.isLottery) {
  165. return
  166. }
  167. this.isLottery = true
  168. let len = this.options.length
  169. if (len == 0) {
  170. return;
  171. }
  172. //console.log("playReward")
  173. // if (this.beforePlay) {
  174. // () => this.beforePlay()
  175. // this.beforePlay()
  176. this.$emit("beforePlay")
  177. // }
  178. let num = -1;
  179. if (this.setWinnerFn) {
  180. // 自定义抽奖结果
  181. let res = () => this.setWinnerFn(this.options)
  182. if (res != undefined && res >= 0) {
  183. num = res
  184. }
  185. }
  186. if (num < 0) {
  187. // 进行权重抽奖
  188. let optionIndex = this.lotteryWeight(this.options)
  189. if (optionIndex != undefined || optionIndex >= 0) {
  190. num = optionIndex
  191. }
  192. }
  193. if (num < 0) {
  194. // 没有自动抽奖结果 && 没有定义选项权重,则进行随机数抽奖
  195. num = Math.floor(Math.random() * len)
  196. }
  197. const result = await this.roateCanvas(num)
  198. //console.log("抽奖结果:", result.name)
  199. this.name=result.name
  200. this.isLottery = false
  201. let title = ""
  202. let content = result.name
  203. if (this.turnModalContent.length > 0) {
  204. // 若设置了 content ,则随机取一个 content 内容,并且 title 将展示为抽奖结果
  205. title = result.name
  206. content = this.turnModalContent[Math.floor(Math.random() * this.turnModalContent.length)]
  207. }
  208. },
  209. initBtnCanvas() {
  210. let angleTo = 0
  211. let ctx = uni.createCanvasContext("canvasBtn", this);
  212. // 6. 画中心点圆
  213. // 圆中心点的坐标 x: 宽度的一半
  214. let center_x = uni.upx2px(this.width) / 2;
  215. // 圆中心点的坐标 y: 高度的一半
  216. let center_y = uni.upx2px(this.height) / 2;
  217. // 1. 先清除画布上在该矩形区域内的内容
  218. ctx.clearRect(0, 0, uni.upx2px(this.width), uni.upx2px(this.height));
  219. ctx.translate(center_x, center_y);
  220. // 6. 画中心点圆
  221. ctx.beginPath();
  222. // ctx.arc(0, 0, 15, 0, Math.PI * 2); // 15 为中心点圆的半径
  223. ctx.moveTo(0, -50); // 三角形顶点坐标
  224. ctx.lineTo(-20, 10); // 左下角坐标
  225. ctx.lineTo(20, 10); // 右下角坐标
  226. ctx.setFillStyle("#ff0000");
  227. ctx.fill();
  228. ctx.draw();
  229. },
  230. initCanvas: function(ctx, angleTo) {
  231. const len = this.options.length; //数组长度
  232. if (len == 0) {
  233. //console.log("options len == 0")
  234. return;
  235. }
  236. if (!angleTo) {
  237. angleTo = 0
  238. }
  239. // 圆中心点的坐标 x: 宽度的一半
  240. let center_x = uni.upx2px(this.width) / 2;
  241. // 圆中心点的坐标 y: 高度的一半
  242. let center_y = uni.upx2px(this.height) / 2;
  243. // 圆的弧度的总度数,2π表示画圆
  244. let totalAngle = 2 * Math.PI;
  245. // 平均一个选项占用的孤度数
  246. let avgAngle = totalAngle / len;
  247. let radius = center_x - 14;
  248. let fontSize = this.getFontSize()
  249. // 1. 先清除画布上在该矩形区域内的内容
  250. ctx.clearRect(0, 0,uni.upx2px( this.width),uni.upx2px( this.height));
  251. ctx.translate(center_x, center_y);
  252. // 2. 设置画布内字体大小
  253. ctx.setFontSize(fontSize);
  254. ctx.setLineWidth(14);
  255. ctx.save();
  256. // 3. 画外圆
  257. ctx.rotate(angleTo * Math.PI / 180);
  258. var beginAngle = 2 * Math.PI / 360 * (-90);
  259. // ctx.setStrokeStyle("#ffaa00");
  260. ctx.setStrokeStyle("#ffffff");
  261. ctx.arc(0, 0, radius - 3, 0, Math.PI * 2);
  262. ctx.stroke();
  263. // 4. 划分区域,并且填充颜色
  264. ctx.setLineWidth(0.1);
  265. beginAngle = 2 * Math.PI / 360 * (-90);
  266. //绘制填充形状
  267. for (var i = 0; i < len; i++) {
  268. ctx.save();
  269. ctx.beginPath();
  270. ctx.moveTo(0, 0);
  271. ctx.setStrokeStyle(this.options[i].color);
  272. ctx.setFillStyle(this.options[i].color);
  273. ctx.arc(0, 0, radius, beginAngle, beginAngle + avgAngle, false);
  274. //ctx.stroke();
  275. beginAngle = beginAngle + avgAngle;
  276. ctx.fill();
  277. ctx.save();
  278. }
  279. // 5. 绘制选项文字
  280. beginAngle = 0; //avgAngle / 2;
  281. for (var i = 0; i < len; i++) {
  282. var ry = -(center_x / 2) - 25;
  283. //绘制旋转文字
  284. ctx.rotate((beginAngle + (avgAngle * 0.5))); //顺时针旋转
  285. ctx.setTextAlign("center");
  286. ctx.setFillStyle("#9D3A0F");
  287. ctx.fillText(this.options[i].name, 0, ry);
  288. ctx.restore();
  289. beginAngle = beginAngle + avgAngle;
  290. }
  291. var img = new Image();
  292. // img.onload = function() {
  293. beginAngle = 0; //avgAngle / 2;
  294. for (var i = 0; i < len; i++) {
  295. ctx.save();
  296. var ry = -(center_x / 2) - 25;
  297. //绘制旋转文字
  298. ctx.rotate((beginAngle + (avgAngle * 0.5))); //顺时针旋转
  299. if(this.options[i].id==1){
  300. ctx.drawImage(this.img2,uni.upx2px(-17*2) ,uni.upx2px( -90*2), uni.upx2px(64), uni.upx2px(64));
  301. }else{
  302. ctx.drawImage(this.img3,uni.upx2px(-17*2) ,uni.upx2px( -90*2), uni.upx2px(64), uni.upx2px(64));
  303. }
  304. // ctx.draw(true)
  305. ctx.restore();
  306. beginAngle = beginAngle + avgAngle;
  307. }
  308. //ctx.draw();
  309. // }
  310. // img.src=this.img3
  311. ctx.save();
  312. // 6. 画中心点圆
  313. ctx.beginPath();
  314. ctx.arc(0, 0, 15, 0, Math.PI * 2); // 15 为中心点圆的半径
  315. ctx.setFillStyle("#FFFFFF");
  316. ctx.fill();
  317. ctx.draw();
  318. },
  319. // 根据设置的权重进行抽奖
  320. lotteryWeight(prizes) {
  321. if (!prizes || prizes.length == 0) {
  322. //console.log("奖品列表为空")
  323. return -1
  324. }
  325. let winPrizesIndex = [] // 抽中的奖品下标,多个是因为如果存在多个奖品的权重一致的情况,则再进行随机抽奖
  326. let winPrizeWeight = 0; // 抽中的奖品的权重
  327. let round = Math.random() * 100; // 生成一个 0-100的随机数
  328. //console.log("lotteryWeight 生成的 round:", round)
  329. for (let index = 0; index < prizes.length; index++) {
  330. let prize = prizes[index];
  331. let weight = prize['weight']
  332. if (!weight) {
  333. // 没有设置权重,则跳过
  334. //console.log("奖品 ", prize.name, " 未设置权重,则不参与抽奖")
  335. continue
  336. }
  337. if (weight <= 0) {
  338. // 如果奖品的权重设置<0,则表示不参与抽奖
  339. //console.log("奖品 ", prize.name, " 不参与抽奖")
  340. continue
  341. }
  342. if (round > weight) {
  343. // 随机数超过了权重,则未抽中
  344. continue
  345. }
  346. if (weight < winPrizeWeight) {
  347. // 权重比之前抽中的还小,则跳过
  348. //console.log("奖品 ", prize.name, "小于已经抽中的奖品", winPrizeWeight, " 不参与抽奖")
  349. continue
  350. }
  351. if (weight == winPrizeWeight) {
  352. // 本次抽中的奖品和已经抽中的奖品权重一致,则加入抽中列表
  353. //console.log("再抽中奖品 ", prize.name, " 中奖")
  354. winPrizesIndex.push(index)
  355. continue
  356. }
  357. if (weight > winPrizeWeight) {
  358. // 权重比之前抽中的还大,则重置抽中奖品
  359. winPrizesIndex = [index]
  360. winPrizeWeight = weight
  361. //console.log("奖品 ", prize.name, " 中奖")
  362. continue
  363. }
  364. }
  365. if (winPrizesIndex.length <= 0) {
  366. // 本次没有抽中奖品
  367. //console.log("本次没有抽中奖品")
  368. return -1
  369. }
  370. if (winPrizesIndex.length == 1) {
  371. // 只抽中了一个奖品,这直接返回
  372. let index = winPrizesIndex[0];
  373. //console.log("奖品 ", prizes[index], " 抽中");
  374. return index
  375. }
  376. if (winPrizesIndex.length > 1) {
  377. // 抽中多个,则再进行随机抽奖
  378. //console.log(" 抽中多个,则再进行随机抽奖");
  379. let index = round % winPrizesIndex.length
  380. index = Math.floor(index)
  381. //console.log("再次抽中结果:", winPrizesIndex[index])
  382. return winPrizesIndex[index]
  383. }
  384. //console.log("其他抽奖情况")
  385. return -1
  386. },
  387. // 旋转画布,num 表示选项 options 的下标
  388. roateCanvas(num) {
  389. let len = this.options.length
  390. let angle = 360 / len;
  391. angle = num * angle + angle / 2;
  392. angle = angle || 0;
  393. angle = 360 - angle;
  394. angle += 360 * 5;
  395. if (this.turnCircle > 0) {
  396. let turnCircle = this.turnCircle
  397. // 最多只能转 10 圈
  398. if (turnCircle > 10) {
  399. turnCircle = 10
  400. }
  401. angle += 360 * turnCircle;
  402. }
  403. let that = this;
  404. let count = 1;
  405. // 减速,值越小,减速效果越明显
  406. let turnReduceSpeed = this.turnReduceSpeed
  407. if (turnReduceSpeed == 0) {
  408. turnReduceSpeed = 1
  409. }
  410. let baseStep = turnReduceSpeed;
  411. // 起始滚动速度
  412. let baseSpeed = 1;
  413. let result = {}
  414. return new Promise((resolve, reject) => {
  415. let timer = setInterval(function() {
  416. that.initCanvas(that.ctx, count);
  417. if (count == angle) {
  418. clearInterval(timer);
  419. result = that.options[num];
  420. resolve(result)
  421. }
  422. count = count + baseStep * (((angle - count) / angle) > baseSpeed ? baseSpeed :
  423. ((angle -
  424. count) / angle)) + 0.1;
  425. if (angle - count < 0.5) {
  426. count = angle;
  427. }
  428. }, 25);
  429. });
  430. },
  431. getFontSize() {
  432. let fontSize = this.fontSize
  433. if (this.options.length > 10) {
  434. if (this.fontSize >= 18) {
  435. fontSize = this.fontSize - (this.options.length - 10)
  436. }
  437. }
  438. return uni.upx2px(fontSize*2)
  439. },
  440. // 生成随机颜色
  441. genRandColor() {
  442. // 生成随机的 RGB 值
  443. var r = Math.floor(Math.random() * 256); // 0 到 255 之间的随机数
  444. var g = Math.floor(Math.random() * 256);
  445. var b = Math.floor(Math.random() * 256);
  446. // 将 RGB 值转换为 Hex 颜色表示
  447. var hexColor = "#" + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b);
  448. return hexColor;
  449. },
  450. componentToHex(c) {
  451. var hex = c.toString(16);
  452. return hex.length === 1 ? "0" + hex : hex;
  453. },
  454. initOptions: function() {
  455. let defaultOptions = this.options
  456. if (this.isUseDefaultOptions) {
  457. // 使用默认数据
  458. } else {
  459. this.options = this.data
  460. }
  461. // 所有默认的颜色
  462. let allDefColorArr = defaultOptions.map(item => item.color);
  463. // 找到最大的 id
  464. let maxId = -Infinity;
  465. this.options.forEach(item => {
  466. if (!item.id) {
  467. return
  468. }
  469. if (item.id > maxId) {
  470. maxId = item.id;
  471. }
  472. });
  473. // 填充 id,确保 id 唯一
  474. var existIds = []
  475. for (var i = 0; i < this.options.length; i++) {
  476. let item = this.options[i]
  477. if (item['id'] == undefined || item.id == 0 || existIds.includes(item.id)) {
  478. // id 不存在,或者 id 重复了
  479. this.options[i]['id'] = maxId + 1
  480. }
  481. existIds.push(this.options[i].id)
  482. if (item['color'] == undefined || item.color == "") {
  483. }
  484. }
  485. // 填充颜色,确保颜色唯一
  486. let availableColor = JSON.parse(JSON.stringify(allDefColorArr)) // 可用颜色
  487. let existColor = [] // 存在颜色
  488. for (var i = 0; i < this.options.length; i++) {
  489. let item = this.options[i]
  490. if (item['color'] == undefined || item.color == "") {
  491. continue
  492. }
  493. let color = item.color
  494. existColor.push(color)
  495. // 过滤掉已经用了的 color
  496. availableColor = availableColor.filter(item => item !== color);
  497. }
  498. // 剩下的 allDefColorArr 都是可用的颜色
  499. for (var i = 0; i < this.options.length; i++) {
  500. let item = this.options[i]
  501. if (item['color'] == undefined || item.color == "") {
  502. if (availableColor.length == 0) {
  503. // 没有可用颜色了,则随机生成一个
  504. let color = ''
  505. for (var j = 0; j < 100; j++) {
  506. if (color != '') {
  507. continue
  508. }
  509. let genColor = this.genRandColor()
  510. if (!existColor.includes(genColor)) {
  511. existColor.push(color)
  512. color = genColor
  513. }
  514. }
  515. this.options[i]['color'] = color
  516. continue
  517. }
  518. const color = availableColor.shift();
  519. existColor.push(color)
  520. this.options[i].color = color
  521. }
  522. }
  523. },
  524. init() {
  525. this.initOptions();
  526. this.ctx = uni.createCanvasContext("canvas", this);
  527. this.initCanvas(this.ctx, 0);
  528. this.initBtnCanvas();
  529. this.playReward()
  530. }
  531. },
  532. // 初始化画布
  533. mounted: function() {
  534. //console.log("lottery mounted init......")
  535. this.init()
  536. }
  537. }
  538. </script>
  539. <style scoped>
  540. .main {
  541. display: flex;
  542. flex-direction: column;
  543. justify-content: center;
  544. align-items: center;
  545. }
  546. .title-container {
  547. font-size: 50rpx;
  548. margin-top: 120rpx;
  549. margin-bottom: 40rpx;
  550. z-index: 100;
  551. }
  552. .canvas-container {
  553. position: relative;
  554. width: fit-content;
  555. height: fit-content;
  556. z-index: 99;
  557. }
  558. .canvasBtn {
  559. position: absolute;
  560. top: 0%;
  561. }
  562. .canvas-btn {
  563. position: absolute;
  564. top: 50%;
  565. left: 50%;
  566. background-color: #ffffff;
  567. transform: translate(-50%, -50%);
  568. width: 110rpx;
  569. height: 110rpx;
  570. border-radius: 50%;
  571. line-height: 110rpx;
  572. text-align: center;
  573. font-size: 45rpx;
  574. text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.6);
  575. box-shadow: 0 3px 5px rgba(0, 0, 0, 0.6);
  576. text-decoration: none;
  577. }
  578. /* .canvas-btn::before {
  579. content: "";
  580. position: absolute;
  581. top: -15px;
  582. left: 50%;
  583. transform: translateX(-50%);
  584. width: 0;
  585. height: 0;
  586. border-left: 10px solid transparent;
  587. border-right: 10px solid transparent;
  588. border-bottom: 20px solid #ffffff;
  589. } */
  590. .canvas-btn.isLottery {
  591. background-color: #CCCCCC;
  592. }
  593. /* .canvas-btn.isLottery {
  594. pointer-events: none;
  595. background: #CCCCCC;
  596. color: #ccc;
  597. } */
  598. /* .canvas-btn.isLottery::before {
  599. border-bottom-color: #CCCCCC;
  600. }
  601. .canvas-btn.isLottery::after {
  602. border-bottom-color: #CCCCCC;
  603. } */
  604. </style>