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

657 lines
16 KiB

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