瑶都万能墙
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

702 lines
18 KiB

3 months ago
3 months ago
3 months ago
  1. <template>
  2. <view class="page">
  3. <navbar leftClick
  4. color="#fff"
  5. bgColor="#667eea"
  6. @leftClick="$utils.navigateBack" />
  7. <view class="turntable-container">
  8. <!-- 头部标题 -->
  9. <view class="header">
  10. <text class="title">幸运大转盘</text>
  11. <text class="subtitle">转一转好运来</text>
  12. </view>
  13. <!-- 积分余额显示 -->
  14. <!-- 注释积分显示区域 -->
  15. <!-- <view class="points-display">
  16. <view class="points-info">
  17. <text class="points-label">当前积分</text>
  18. <text class="points-value">{{ userPoints }}</text>
  19. </view>
  20. <view class="cost-info">
  21. <text class="cost-label">每次消耗</text>
  22. <text class="cost-value">{{ drawCost }}积分</text>
  23. </view>
  24. </view> -->
  25. <!-- 转盘区域 -->
  26. <view class="turntable-wrapper" v-if="prizes.length > 0">
  27. <view class="turntable" :class="{ 'spinning': isSpinning }" :style="{ transform: `rotate(${rotateAngle}deg)` }">
  28. <!-- 使用纯CSS创建转盘 -->
  29. <view class="wheel-bg">
  30. <!-- 8个扇形区域 -->
  31. <view
  32. v-for="(prize, index) in prizes"
  33. :key="index"
  34. class="wheel-sector"
  35. :style="{
  36. backgroundColor: sectorColors[index % sectorColors.length],
  37. transform: `rotate(${index * sectorAngle}deg)`
  38. }"
  39. >
  40. </view>
  41. </view>
  42. <!-- 奖品文字覆盖层 -->
  43. <view class="prizes-overlay">
  44. <view
  45. v-for="(prize, index) in prizes"
  46. :key="index"
  47. class="prize-item"
  48. :style="prizeStyles[index]"
  49. >
  50. <view class="prize-content">
  51. <text class="prize-icon">{{ getIcon(prize.type) }}</text>
  52. <text class="prize-name">{{ prize.title }}</text>
  53. <text class="prize-value" v-if="prize.price">{{ prize.price }}</text>
  54. </view>
  55. </view>
  56. </view>
  57. </view>
  58. <!-- 中心指针 -->
  59. <view class="pointer">
  60. <view class="pointer-triangle"></view>
  61. </view>
  62. <!-- 中心按钮 -->
  63. <view class="center-button" @click="startSpin" :class="{ disabled: isSpinning || userPoints < drawCost || isWatchingAd }">
  64. <text class="button-text">{{ getButtonText() }}</text>
  65. </view>
  66. </view>
  67. <!-- 加载中状态 -->
  68. <view v-else class="loading-wrapper">
  69. <uv-loading-icon mode="spinner" color="#fff" size="60"></uv-loading-icon>
  70. <text class="loading-text">加载中...</text>
  71. </view>
  72. <!-- 抽奖结果弹窗 -->
  73. <view class="result-modal" v-if="showResult" @click="closeResult">
  74. <view class="modal-content" @click.stop>
  75. <text class="result-title">🎉 恭喜您 🎉</text>
  76. <view class="result-prize">
  77. <!-- <text class="result-icon">{{ getIcon(currentPrize.type) }}</text> -->
  78. <image :src="currentPrize.img" mode="widthFix" style="width: 100%;"></image>
  79. <text class="result-name">{{ currentPrize.title }}</text>
  80. <text class="result-value" v-if="currentPrize.price">{{ currentPrize.price }}</text>
  81. </view>
  82. <view class="points-change">
  83. <text class="change-text">{{ getPointsChangeText() }}</text>
  84. </view>
  85. <view class="result-actions">
  86. <button class="confirm-btn" @click="closeResult">确定</button>
  87. </view>
  88. </view>
  89. </view>
  90. <!-- 积分提示 -->
  91. <!-- 注释积分提示信息 -->
  92. <!-- <view class="spin-info">
  93. <text>观看完整视频广告后消耗{{ drawCost }}积分即可抽奖快来试试手气吧</text>
  94. </view> -->
  95. <view class="spin-info">
  96. <text>观看完整视频广告即可免费抽奖快来试试手气吧</text>
  97. </view>
  98. </view>
  99. </view>
  100. </template>
  101. <script>
  102. import { mapState } from 'vuex'
  103. import rewardedVideoAdMixin from '@/mixins/rewardedVideoAd.js'
  104. export default {
  105. mixins: [rewardedVideoAdMixin],
  106. computed: {
  107. ...mapState(['userInfo']),
  108. // 获取用户当前积分
  109. userPoints() {
  110. return this.userInfo?.integerPrice || 0
  111. }
  112. },
  113. data() {
  114. return {
  115. // 奖品配置(从接口获取)
  116. prizes: [],
  117. sectorColors: [
  118. '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
  119. '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'
  120. ],
  121. isSpinning: false, // 是否正在旋转
  122. rotateAngle: 0, // 旋转角度
  123. totalSpinRounds: 0, // 总旋转圈数
  124. currentAngle: 0, // 当前角度(0-360度)
  125. showResult: false, // 显示结果弹窗
  126. currentPrize: null, // 当前中奖奖品
  127. sectorAngle: 0, // 每个扇形的角度
  128. drawCost: 5, // 抽奖消耗积分
  129. pointsChange: null, // 积分变化
  130. // 预计算奖品位置样式
  131. prizeStyles: []
  132. // 视频广告相关数据已移至混入中
  133. }
  134. },
  135. onLoad() {
  136. this.getLuckDrawList()
  137. // initRewardedVideoAd() 已在混入的mounted中处理
  138. this.rewardedRecordConfig.type = 10 // 10抽奖
  139. },
  140. onShow() {
  141. // 刷新用户信息以获取最新积分
  142. if (uni.getStorageSync('token')) {
  143. this.$store.commit('getUserInfo')
  144. }
  145. },
  146. methods: {
  147. // 广告观看完成回调
  148. onAdWatchComplete() {
  149. console.log('用户看完广告,开始抽奖')
  150. this.performDraw()
  151. },
  152. // 广告观看取消回调
  153. onAdWatchCancel() {
  154. uni.showToast({
  155. title: '请观看完整广告才能抽奖',
  156. icon: 'none'
  157. })
  158. },
  159. // 获取抽奖列表
  160. getLuckDrawList() {
  161. this.$api('getLuckDrawList', {}, res => {
  162. if (res.code == 200) {
  163. this.prizes = res.result || []
  164. // 计算每个扇形的角度
  165. if (this.prizes.length > 0) {
  166. this.sectorAngle = 360 / this.prizes.length
  167. this.calculatePrizeStyles()
  168. }
  169. } else {
  170. uni.showToast({
  171. title: res.message || '获取抽奖信息失败',
  172. icon: 'none'
  173. })
  174. }
  175. })
  176. },
  177. // 获取按钮文字
  178. getButtonText() {
  179. if (this.isWatchingAd) {
  180. return '观看广告中...'
  181. }
  182. if (this.isSpinning) {
  183. return '抽奖中...'
  184. }
  185. // 注释积分不足检查
  186. // if (this.userPoints < this.drawCost) {
  187. // return '积分不足'
  188. // }
  189. return '观看广告抽奖'
  190. },
  191. // 根据奖品类型获取图标
  192. getIcon(type) {
  193. const iconMap = {
  194. '0': '💰',
  195. 'points': '⭐',
  196. 'gift': '🎁',
  197. 'coupon': '🎫',
  198. 'thanks': '🤝'
  199. }
  200. return iconMap[type] || '🎁'
  201. },
  202. // 计算奖品位置样式
  203. calculatePrizeStyles() {
  204. this.prizeStyles = this.prizes.map((prize, index) => {
  205. // 计算奖品在圆形中的位置
  206. const angle = (index * this.sectorAngle + this.sectorAngle / 2) * Math.PI / 180; // 转换为弧度
  207. const radius = 150; // 奖品距离中心的距离
  208. const x = Math.cos(angle - Math.PI/2) * radius; // 减去90度,因为我们希望0度在顶部
  209. const y = Math.sin(angle - Math.PI/2) * radius;
  210. return `left: calc(50% + ${x}rpx); top: calc(50% + ${y}rpx); transform: translate(-50%, -50%);`
  211. })
  212. },
  213. // 开始抽奖
  214. startSpin() {
  215. // 检查是否可以抽奖
  216. if (this.isSpinning || this.isWatchingAd) {
  217. return
  218. }
  219. // 注释积分检查逻辑
  220. // // 检查积分是否足够
  221. // if (this.userPoints < this.drawCost) {
  222. // uni.showModal({
  223. // title: '积分不足',
  224. // content: `抽奖需要消耗${this.drawCost}积分,您当前积分为${this.userPoints}`,
  225. // showCancel: true,
  226. // cancelText: '取消',
  227. // confirmText: '去赚积分',
  228. // success: (res) => {
  229. // if (res.confirm) {
  230. // // 跳转到积分获取页面或任务页面
  231. // uni.navigateBack()
  232. // }
  233. // }
  234. // })
  235. // return
  236. // }
  237. // 先播放广告,再进行抽奖
  238. this.showRewardedVideoAd({
  239. onSuccess: this.onAdWatchComplete,
  240. onCancel: this.onAdWatchCancel,
  241. fallbackTitle: '广告加载失败',
  242. fallbackContent: '无法加载广告,是否直接抽奖?'
  243. })
  244. },
  245. // 执行抽奖(观看广告后调用)
  246. performDraw() {
  247. this.isSpinning = true
  248. // 调用抽奖接口
  249. this.$api('luckDraw', {}, res => {
  250. if (res.code == 200) {
  251. const result = res.result
  252. this.currentPrize = result.gift
  253. this.pointsChange = result.remainingIntegral || null
  254. // 更新用户信息(积分变化)
  255. this.$store.commit('getUserInfo')
  256. // 找到中奖奖品的索引
  257. const prizeIndex = this.prizes.findIndex(prize => prize.id == result.gift.id)
  258. if (prizeIndex !== -1) {
  259. // 计算目标角度
  260. // 奖品在扇形中心的角度
  261. const prizeAngle = prizeIndex * this.sectorAngle + this.sectorAngle / 2
  262. // 要让奖品转到指针位置(0度),目标角度就是负的奖品角度
  263. let targetAngle = -prizeAngle
  264. // 确保目标角度为正值(0-360度范围内)
  265. if (targetAngle < 0) {
  266. targetAngle += 360
  267. }
  268. // 增加旋转圈数
  269. const spinRounds = 5 // 转5圈
  270. this.totalSpinRounds += spinRounds
  271. this.currentAngle = targetAngle
  272. // 计算最终的旋转角度 = 总圈数 * 360 + 当前角度
  273. this.rotateAngle = this.totalSpinRounds * 360 + this.currentAngle
  274. // 动画结束后显示结果
  275. setTimeout(() => {
  276. this.isSpinning = false
  277. this.showResult = true
  278. this.handlePrizeResult()
  279. }, 3000)
  280. } else {
  281. this.isSpinning = false
  282. uni.showToast({
  283. title: '抽奖异常,请重试',
  284. icon: 'none'
  285. })
  286. }
  287. } else {
  288. this.isSpinning = false
  289. uni.showToast({
  290. title: res.message || '抽奖失败,请重试',
  291. icon: 'none'
  292. })
  293. }
  294. })
  295. },
  296. // 处理中奖结果
  297. handlePrizeResult() {
  298. if (this.currentPrize) {
  299. console.log(`中奖信息:`, this.currentPrize)
  300. // 根据奖品类型显示不同提示
  301. if (this.currentPrize.type === 'money') {
  302. console.log(`获得现金奖励: ${this.currentPrize.prizeValue}`)
  303. } else if (this.currentPrize.type === 'points') {
  304. console.log(`获得积分奖励: ${this.currentPrize.prizeValue}`)
  305. } else if (this.currentPrize.type === 'gift') {
  306. console.log(`获得礼品: ${this.currentPrize.prizeName}`)
  307. }
  308. }
  309. },
  310. // 关闭结果弹窗
  311. closeResult() {
  312. this.showResult = false
  313. this.pointsChange = null
  314. // 刷新抽奖信息
  315. this.getLuckDrawList()
  316. },
  317. // 积分变化提示文字
  318. getPointsChangeText() {
  319. // 注释积分消耗提示
  320. // let text = `消耗 ${this.drawCost} 积分`
  321. let text = `恭喜您获得奖品!`
  322. if (this.currentPrize && this.currentPrize.type === 'points') {
  323. const gained = parseInt(this.currentPrize.prizeValue)
  324. // 注释积分计算逻辑
  325. // const net = gained - this.drawCost
  326. // if (net > 0) {
  327. // text += `,获得 ${gained} 积分,净收益 +${net} 积分`
  328. // } else if (net < 0) {
  329. // text += `,获得 ${gained} 积分,净损失 ${Math.abs(net)} 积分`
  330. // } else {
  331. // text += `,获得 ${gained} 积分,收支平衡`
  332. // }
  333. text = `获得 ${gained} 积分奖励!`
  334. }
  335. return text
  336. }
  337. }
  338. }
  339. </script>
  340. <style scoped lang="scss">
  341. .turntable-container {
  342. min-height: 100vh;
  343. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  344. padding: 40rpx;
  345. display: flex;
  346. flex-direction: column;
  347. align-items: center;
  348. }
  349. .header {
  350. text-align: center;
  351. margin-bottom: 40rpx;
  352. color: white;
  353. .title {
  354. font-size: 48rpx;
  355. font-weight: bold;
  356. display: block;
  357. margin-bottom: 20rpx;
  358. }
  359. .subtitle {
  360. font-size: 28rpx;
  361. opacity: 0.9;
  362. }
  363. }
  364. .points-display {
  365. background: rgba(255, 255, 255, 0.2);
  366. border-radius: 20rpx;
  367. padding: 30rpx;
  368. margin-bottom: 40rpx;
  369. backdrop-filter: blur(10rpx);
  370. display: flex;
  371. justify-content: space-between;
  372. align-items: center;
  373. width: 100%;
  374. max-width: 600rpx;
  375. .points-info, .cost-info {
  376. display: flex;
  377. align-items: center;
  378. color: white;
  379. .points-label, .cost-label {
  380. font-size: 28rpx;
  381. margin-right: 10rpx;
  382. }
  383. .points-value {
  384. font-size: 32rpx;
  385. font-weight: bold;
  386. color: #FFD700;
  387. }
  388. .cost-value {
  389. font-size: 28rpx;
  390. font-weight: bold;
  391. color: #FF6B6B;
  392. }
  393. }
  394. }
  395. .turntable-wrapper {
  396. position: relative;
  397. width: 600rpx;
  398. height: 600rpx;
  399. margin-bottom: 60rpx;
  400. }
  401. .loading-wrapper {
  402. display: flex;
  403. flex-direction: column;
  404. align-items: center;
  405. justify-content: center;
  406. height: 600rpx;
  407. .loading-text {
  408. color: white;
  409. font-size: 28rpx;
  410. margin-top: 20rpx;
  411. }
  412. }
  413. .turntable {
  414. width: 100%;
  415. height: 100%;
  416. border-radius: 50%;
  417. position: relative;
  418. transition: transform 3s cubic-bezier(0.23, 1, 0.32, 1);
  419. box-shadow: 0 0 40rpx rgba(0, 0, 0, 0.3);
  420. overflow: hidden;
  421. &.spinning {
  422. transition-duration: 3s;
  423. }
  424. }
  425. .wheel-bg {
  426. position: absolute;
  427. width: 100%;
  428. height: 100%;
  429. border-radius: 50%;
  430. overflow: hidden;
  431. }
  432. .wheel-sector {
  433. position: absolute;
  434. width: 50%;
  435. height: 50%;
  436. top: 0%;
  437. left: 50%;
  438. transform-origin: 0% 100%;
  439. &::before {
  440. content: '';
  441. position: absolute;
  442. top: 0;
  443. left: 0;
  444. width: 100%;
  445. height: 100%;
  446. background: inherit;
  447. clip-path: polygon(0% 100%, 50% 0%, 100% 100%);
  448. }
  449. // 添加边框线
  450. &::after {
  451. content: '';
  452. position: absolute;
  453. top: 0;
  454. left: 0;
  455. width: 100%;
  456. height: 100%;
  457. border-right: 2rpx solid rgba(255,255,255,0.3);
  458. transform-origin: 0% 100%;
  459. }
  460. }
  461. .prizes-overlay {
  462. position: absolute;
  463. width: 100%;
  464. height: 100%;
  465. top: 0;
  466. left: 0;
  467. z-index: 990;
  468. }
  469. .prize-item {
  470. position: absolute;
  471. width: 120rpx;
  472. height: 80rpx;
  473. z-index: 10;
  474. }
  475. .prize-content {
  476. width: 100%;
  477. height: 100%;
  478. display: flex;
  479. flex-direction: column;
  480. align-items: center;
  481. justify-content: center;
  482. text-align: center;
  483. color: white;
  484. text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.8);
  485. .prize-icon {
  486. font-size: 32rpx;
  487. display: block;
  488. margin-bottom: 4rpx;
  489. }
  490. .prize-name {
  491. font-size: 18rpx;
  492. display: block;
  493. font-weight: bold;
  494. margin-bottom: 2rpx;
  495. line-height: 1.2;
  496. }
  497. .prize-value {
  498. font-size: 20rpx;
  499. display: block;
  500. font-weight: bold;
  501. line-height: 1.2;
  502. }
  503. }
  504. .pointer {
  505. position: absolute;
  506. top: -20rpx;
  507. left: 50%;
  508. transform: translateX(-50%);
  509. z-index: 100;
  510. .pointer-triangle {
  511. width: 0;
  512. height: 0;
  513. border-left: 20rpx solid transparent;
  514. border-right: 20rpx solid transparent;
  515. border-top: 60rpx solid #FF4757;
  516. filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.3));
  517. }
  518. }
  519. .center-button {
  520. position: absolute;
  521. top: 50%;
  522. left: 50%;
  523. transform: translate(-50%, -50%);
  524. width: 160rpx;
  525. height: 160rpx;
  526. border-radius: 50%;
  527. background: linear-gradient(145deg, #FF6B6B, #FF4757);
  528. display: flex;
  529. align-items: center;
  530. justify-content: center;
  531. box-shadow: 0 8rpx 20rpx rgba(255, 71, 87, 0.4);
  532. z-index: 50;
  533. transition: transform 0.2s;
  534. &:active:not(.disabled) {
  535. transform: translate(-50%, -50%) scale(0.95);
  536. }
  537. &.disabled {
  538. opacity: 0.6;
  539. cursor: not-allowed;
  540. background: linear-gradient(145deg, #999, #777);
  541. }
  542. .button-text {
  543. color: white;
  544. font-size: 24rpx;
  545. font-weight: bold;
  546. text-align: center;
  547. }
  548. }
  549. .result-modal {
  550. position: fixed;
  551. top: 0;
  552. left: 0;
  553. width: 100vw;
  554. height: 100vh;
  555. background: rgba(0, 0, 0, 0.7);
  556. display: flex;
  557. align-items: center;
  558. justify-content: center;
  559. z-index: 1000;
  560. .modal-content {
  561. background: white;
  562. border-radius: 20rpx;
  563. padding: 60rpx 40rpx;
  564. text-align: center;
  565. box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.3);
  566. min-width: 500rpx;
  567. .result-title {
  568. font-size: 36rpx;
  569. font-weight: bold;
  570. color: #FF6B6B;
  571. margin-bottom: 40rpx;
  572. }
  573. .result-prize {
  574. margin-bottom: 30rpx;
  575. .result-icon {
  576. font-size: 60rpx;
  577. display: block;
  578. margin-bottom: 20rpx;
  579. }
  580. .result-name {
  581. font-size: 32rpx;
  582. color: #333;
  583. display: block;
  584. margin-bottom: 10rpx;
  585. }
  586. .result-value {
  587. font-size: 36rpx;
  588. color: #FF6B6B;
  589. font-weight: bold;
  590. }
  591. }
  592. .points-change {
  593. margin-bottom: 40rpx;
  594. padding: 20rpx;
  595. background: #f5f5f5;
  596. border-radius: 10rpx;
  597. .change-text {
  598. font-size: 24rpx;
  599. color: #666;
  600. line-height: 1.4;
  601. }
  602. }
  603. .confirm-btn {
  604. background: linear-gradient(45deg, #FF6B6B, #FF4757);
  605. color: white;
  606. border: none;
  607. border-radius: 40rpx;
  608. padding: 20rpx 60rpx;
  609. font-size: 28rpx;
  610. font-weight: bold;
  611. }
  612. }
  613. }
  614. .spin-info {
  615. text-align: center;
  616. color: white;
  617. font-size: 28rpx;
  618. background: rgba(255, 255, 255, 0.2);
  619. padding: 20rpx 40rpx;
  620. border-radius: 40rpx;
  621. backdrop-filter: blur(10rpx);
  622. }
  623. </style>