瑶都万能墙
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.

643 lines
16 KiB

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