index11.vue 18 KB

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