Browse Source

feat(抽奖): 实现大转盘抽奖功能及积分消耗逻辑

refactor(列表): 优化列表加载逻辑并支持更多数据格式
fix(钱包): 修复跳转流水页面前更新用户信息的问题
style(表单): 统一学校字段命名规范
docs(API): 新增抽奖相关接口文档
master
前端-胡立永 1 week ago
parent
commit
184e200752
9 changed files with 423 additions and 114 deletions
  1. +22
    -0
      api/api.js
  2. +2
    -2
      mixins/list.js
  3. +107
    -0
      mixins/loadList.js
  4. +1
    -1
      pages/index/center.vue
  5. +4
    -3
      pages/index/index.vue
  6. +6
    -6
      pages_order/auth/wxUserInfo.vue
  7. +262
    -66
      pages_order/marketing/turntable.vue
  8. +16
    -35
      pages_order/mine/customerService.vue
  9. +3
    -1
      pages_order/mine/purse.vue

+ 22
- 0
api/api.js View File

@ -419,6 +419,28 @@ const config = {
method: 'GET',
auth : true,
},
// 新-获取我的客服信息列表
getMyService : {
url: '/token/getMyService',
method: 'GET',
auth : true,
},
// 新-获取大转盘抽奖列表
getLuckDrawList : {
url: '/token/getLuckDrawList',
method: 'GET',
auth : true,
},
// 新-大转盘抽奖
luckDraw : {
url: '/token/luckDraw',
method: 'POST',
auth : true,
showLoading : true,
},
}
const models = ['order']


+ 2
- 2
mixins/list.js View File

@ -55,9 +55,9 @@ export default {
success(res.result)
this[this.mixinsListKey || 'list'] = res.result.records
this[this.mixinsListKey || 'list'] = res.result.records || res.result
this.total = res.result.total
this.total = res.result.total || res.result.length
}
})
})


+ 107
- 0
mixins/loadList.js View File

@ -0,0 +1,107 @@
function query(self, queryParams){
// return (self.beforeGetData && self.beforeGetData()) ||
// queryParams || self.queryParams
// 深度合并对象
return self.$utils.deepMergeObject(
self.$utils.deepMergeObject(self.queryParams,
(self.beforeGetData && self.beforeGetData()) || {}),
queryParams)
}
export default {
data() {
return {
queryParams: {
pageNo: 1,
pageSize: 10,
},
total : 0,
List : [],
hasMore: true, // 是否还有更多数据
loading: false, // 是否正在加载
}
},
onPullDownRefresh() {
this.refreshList()
},
onReachBottom() {
this.loadMore()
},
onLoad() {
this.refreshList()
},
methods: {
// 刷新列表
refreshList() {
this.queryParams.pageNo = 1;
this.hasMore = true;
this.List = [];
this.loadList();
},
// 加载更多
loadMore() {
console.log(this.hasMore , this.loading);
if (!this.hasMore || this.loading) return;
this.queryParams.pageNo++;
this.loadList();
},
// 加载订单列表
loadList() {
return new Promise((success, error) => {
if(!this.mixinsListApi){
return console.error('mixinsListApi 缺失');
}
if (this.loading) return;
this.loading = true;
const params = {
...this.queryParams,
};
this.$api(this.mixinsListApi, query(this, params), res => {
this.loading = false;
uni.stopPullDownRefresh();
if (res.code === 200 && res.result) {
this.getDataThen && this.getDataThen(res.result.records, res.result.total, res.result)
success(res.result)
const newList = res.result.records || [];
this.total = res.result.total
if (this.pageNo === 1) {
this.List = newList;
} else {
this.List = [...this.List, ...newList];
}
// 判断是否还有更多数据
this.hasMore = newList.length >= this.queryParams.pageSize;
} else {
uni.showToast({
title: res.message || '加载失败',
icon: 'none'
});
error(res)
}
})
})
},
}
}

+ 1
- 1
pages/index/center.vue View File

@ -260,7 +260,7 @@
// token
const token = uni.getStorageSync('token')
if (token) {
this.mixinsListApi = 'getMyPostPage'
this.mixinsListApi = this.apiList[this.type]
this.isLogin = true
this.$store.commit('getUserInfo')
this.getData()


+ 4
- 3
pages/index/index.vue View File

@ -133,7 +133,7 @@
<!-- 动态列表 -->
<view class="dynamicList">
<dynamicItem :key="index" v-for="(item, index) in list" :item="item"
<dynamicItem :key="index" v-for="(item, index) in List" :item="item"
@click="$utils.navigateTo('/pages_order/post/postDetail?id=' + item.id)" />
</view>
</view>
@ -181,7 +181,8 @@
import sharePopup from '@/components/user/sharePopup.vue'
import signInOnePopup from '@/components/user/signInOnePopup.vue'
import dynamicItem from '@/components/list/dynamic/dynamicItem.vue'
import mixinsList from '@/mixins/list.js'
// import mixinsList from '@/mixins/list.js'
import mixinsList from '@/mixins/loadList.js'
import {
mapState
} from 'vuex'
@ -250,7 +251,7 @@
} else {
delete this.queryParams.classId
}
this.getData()
this.refreshList()
},
//
onSubscribeMessageTap(){


+ 6
- 6
pages_order/auth/wxUserInfo.vue View File

@ -87,12 +87,12 @@
<input type="text"
placeholder="初中学校名字(选填)"
class="school-input"
v-model="form.middleSchool" />
v-model="form.czSchool" />
<input type="text"
placeholder="高中学校名字(选填)"
class="school-input"
v-model="form.highSchool" />
v-model="form.gzSchool" />
<view class="line">
<view class="">
@ -150,8 +150,8 @@
yearDate : this.$dayjs().add(-18, 'y').valueOf(),//18
address : '',
phone : '',
middleSchool: '',
highSchool: '',
czSchool: '',
gzSchool: '',
},
maxDate : this.$dayjs().valueOf(),
minDate : this.$dayjs().add(-100, 'y').valueOf(),
@ -247,9 +247,9 @@
this.form.address = res.result.address || this.form.address
this.form.middleSchool = res.result.middleSchool || this.form.middleSchool
this.form.czSchool = res.result.czSchool || this.form.czSchool
this.form.highSchool = res.result.highSchool || this.form.highSchool
this.form.gzSchool = res.result.gzSchool || this.form.gzSchool
}
})
},


+ 262
- 66
pages_order/marketing/turntable.vue View File

@ -13,8 +13,20 @@
<text class="subtitle">转一转好运来</text>
</view>
<!-- 积分余额显示 -->
<view class="points-display">
<view class="points-info">
<text class="points-label">当前积分</text>
<text class="points-value">{{ userPoints }}</text>
</view>
<view class="cost-info">
<text class="cost-label">每次消耗</text>
<text class="cost-value">{{ drawCost }}积分</text>
</view>
</view>
<!-- 转盘区域 -->
<view class="turntable-wrapper">
<view class="turntable-wrapper" v-if="prizes.length > 0">
<view class="turntable" :class="{ 'spinning': isSpinning }" :style="{ transform: `rotate(${rotateAngle}deg)` }">
<!-- 使用纯CSS创建转盘 -->
<view class="wheel-bg">
@ -24,8 +36,8 @@
:key="index"
class="wheel-sector"
:style="{
backgroundColor: sectorColors[index],
transform: `rotate(${index * 45}deg)`
backgroundColor: sectorColors[index % sectorColors.length],
transform: `rotate(${index * sectorAngle}deg)`
}"
>
</view>
@ -40,9 +52,9 @@
:style="prizeStyles[index]"
>
<view class="prize-content">
<text class="prize-icon">{{ prize.icon }}</text>
<text class="prize-name">{{ prize.name }}</text>
<text class="prize-value">{{ prize.value }}</text>
<text class="prize-icon">{{ getIcon(prize.type) }}</text>
<text class="prize-name">{{ prize.title }}</text>
<text class="prize-value" v-if="prize.price">{{ prize.price }}</text>
</view>
</view>
</view>
@ -54,19 +66,31 @@
</view>
<!-- 中心按钮 -->
<view class="center-button" @click="startSpin" :class="{ disabled: isSpinning }">
<text class="button-text">{{ isSpinning ? '抽奖中...' : '开始抽奖' }}</text>
<view class="center-button" @click="startSpin" :class="{ disabled: isSpinning || userPoints < drawCost }">
<text class="button-text">{{ getButtonText() }}</text>
</view>
</view>
<!-- 加载中状态 -->
<view v-else class="loading-wrapper">
<uv-loading-icon mode="spinner" color="#fff" size="60"></uv-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<!-- 抽奖结果弹窗 -->
<view class="result-modal" v-if="showResult" @click="closeResult">
<view class="modal-content" @click.stop>
<text class="result-title">🎉 恭喜您 🎉</text>
<view class="result-prize">
<text class="result-icon">{{ currentPrize.icon }}</text>
<text class="result-name">{{ currentPrize.name }}</text>
<text class="result-value">{{ currentPrize.value }}</text>
<!-- <text class="result-icon">{{ getIcon(currentPrize.type) }}</text> -->
<image :src="currentPrize.img" mode="widthFix" style="width: 100%;"></image>
<text class="result-name">{{ currentPrize.title }}</text>
<text class="result-value" v-if="currentPrize.price">{{ currentPrize.price }}</text>
</view>
<view class="points-change">
<text class="change-text">{{ getPointsChangeText() }}</text>
</view>
<view class="result-actions">
<button class="confirm-btn" @click="closeResult">确定</button>
@ -74,29 +98,28 @@
</view>
</view>
<!-- 抽奖次数提示 -->
<!-- 积分提示 -->
<view class="spin-info">
<text>今日剩余抽奖次数: {{ remainingSpins }}</text>
<text>消耗{{ drawCost }}积分即可抽奖快来试试手气吧</text>
</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['userInfo']),
//
userPoints() {
return this.userInfo?.integerPrice || 0
}
},
data() {
return {
//
prizes: [
{ name: '现金奖励', value: '¥10', icon: '💰', type: 'money', amount: 10 },
{ name: '精美礼品', value: '小熊', icon: '🧸', type: 'gift', item: 'teddy_bear' },
{ name: '现金奖励', value: '¥5', icon: '💰', type: 'money', amount: 5 },
{ name: '精美礼品', value: '花束', icon: '💐', type: 'gift', item: 'flowers' },
{ name: '现金奖励', value: '¥20', icon: '💰', type: 'money', amount: 20 },
{ name: '精美礼品', value: '巧克力', icon: '🍫', type: 'gift', item: 'chocolate' },
{ name: '现金奖励', value: '¥2', icon: '💰', type: 'money', amount: 2 },
{ name: '精美礼品', value: '香水', icon: '🌸', type: 'gift', item: 'perfume' }
],
//
prizes: [],
sectorColors: [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
'#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'
@ -105,21 +128,71 @@
rotateAngle: 0, //
showResult: false, //
currentPrize: null, //
remainingSpins: 3, //
sectorAngle: 45, // (360/8)
sectorAngle: 0, //
drawCost: 5, //
pointsChange: null, //
//
prizeStyles: []
}
},
onLoad() {
this.calculatePrizeStyles()
this.getLuckDrawList()
},
onShow() {
//
if (uni.getStorageSync('token')) {
this.$store.commit('getUserInfo')
}
},
methods: {
//
getLuckDrawList() {
this.$api('getLuckDrawList', {}, res => {
if (res.code == 200) {
this.prizes = res.result || []
//
if (this.prizes.length > 0) {
this.sectorAngle = 360 / this.prizes.length
this.calculatePrizeStyles()
}
} else {
uni.showToast({
title: res.message || '获取抽奖信息失败',
icon: 'none'
})
}
})
},
//
getButtonText() {
if (this.isSpinning) {
return '抽奖中...'
}
if (this.userPoints < this.drawCost) {
return '积分不足'
}
return '开始抽奖'
},
//
getIcon(type) {
const iconMap = {
'0': '💰',
'points': '⭐',
'gift': '🎁',
'coupon': '🎫',
'thanks': '🤝'
}
return iconMap[type] || '🎁'
},
//
calculatePrizeStyles() {
this.prizeStyles = this.prizes.map((prize, index) => {
//
const angle = (index * 45 + 22.5) * Math.PI / 180; // 22.5
const angle = (index * this.sectorAngle + this.sectorAngle / 2) * Math.PI / 180; //
const radius = 150; //
const x = Math.cos(angle - Math.PI/2) * radius; // 900
const y = Math.sin(angle - Math.PI/2) * radius;
@ -127,59 +200,118 @@
return `left: calc(50% + ${x}rpx); top: calc(50% + ${y}rpx); transform: translate(-50%, -50%);`
})
},
//
startSpin() {
if (this.isSpinning || this.remainingSpins <= 0) {
if (this.remainingSpins <= 0) {
uni.showToast({
title: '今日抽奖次数已用完',
icon: 'none'
})
}
//
if (this.isSpinning) {
return
}
//
if (this.userPoints < this.drawCost) {
uni.showModal({
title: '积分不足',
content: `抽奖需要消耗${this.drawCost}积分,您当前积分为${this.userPoints}`,
showCancel: true,
cancelText: '取消',
confirmText: '去赚积分',
success: (res) => {
if (res.confirm) {
//
uni.navigateBack()
}
}
})
return
}
this.isSpinning = true
this.remainingSpins--
//
const prizeIndex = Math.floor(Math.random() * this.prizes.length)
this.currentPrize = this.prizes[prizeIndex]
//
// 4522.5
const targetAngle = 360 - (prizeIndex * this.sectorAngle + this.sectorAngle / 2)
const spinRounds = 5 // 5
const finalAngle = this.rotateAngle + spinRounds * 360 + targetAngle
this.rotateAngle = finalAngle
//
setTimeout(() => {
this.isSpinning = false
this.showResult = true
this.handlePrizeResult()
}, 3000)
//
this.$api('luckDraw', {}, res => {
if (res.code == 200) {
const result = res.result
this.currentPrize = result.gift
this.pointsChange = result.remainingIntegral || null
//
this.$store.commit('getUserInfo')
//
const prizeIndex = this.prizes.findIndex(prize => prize.id == result.gift.id)
if (prizeIndex !== -1) {
//
const targetAngle = 360 - (prizeIndex * this.sectorAngle + this.sectorAngle / 2)
const spinRounds = 5 // 5
const finalAngle = this.rotateAngle + spinRounds * 360 + targetAngle
this.rotateAngle = finalAngle
//
setTimeout(() => {
this.isSpinning = false
this.showResult = true
this.handlePrizeResult()
}, 3000)
} else {
this.isSpinning = false
uni.showToast({
title: '抽奖异常,请重试',
icon: 'none'
})
}
} else {
this.isSpinning = false
uni.showToast({
title: res.message || '抽奖失败,请重试',
icon: 'none'
})
}
})
},
//
handlePrizeResult() {
if (this.currentPrize.type === 'money') {
//
console.log(`获得现金奖励: ${this.currentPrize.amount}`)
// API
} else if (this.currentPrize.type === 'gift') {
//
console.log(`获得礼品: ${this.currentPrize.item}`)
// API
if (this.currentPrize) {
console.log(`中奖信息:`, this.currentPrize)
//
if (this.currentPrize.type === 'money') {
console.log(`获得现金奖励: ${this.currentPrize.prizeValue}`)
} else if (this.currentPrize.type === 'points') {
console.log(`获得积分奖励: ${this.currentPrize.prizeValue}`)
} else if (this.currentPrize.type === 'gift') {
console.log(`获得礼品: ${this.currentPrize.prizeName}`)
}
}
},
//
closeResult() {
this.showResult = false
this.pointsChange = null
//
this.getLuckDrawList()
},
//
getPointsChangeText() {
let text = `消耗 ${this.drawCost} 积分`
if (this.currentPrize && this.currentPrize.type === 'points') {
const gained = parseInt(this.currentPrize.prizeValue)
const net = gained - this.drawCost
if (net > 0) {
text += `,获得 ${gained} 积分,净收益 +${net} 积分`
} else if (net < 0) {
text += `,获得 ${gained} 积分,净损失 ${Math.abs(net)} 积分`
} else {
text += `,获得 ${gained} 积分,收支平衡`
}
}
return text
}
}
}
@ -197,7 +329,7 @@
.header {
text-align: center;
margin-bottom: 60rpx;
margin-bottom: 40rpx;
color: white;
.title {
@ -213,6 +345,42 @@
}
}
.points-display {
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 40rpx;
backdrop-filter: blur(10rpx);
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 600rpx;
.points-info, .cost-info {
display: flex;
align-items: center;
color: white;
.points-label, .cost-label {
font-size: 28rpx;
margin-right: 10rpx;
}
.points-value {
font-size: 32rpx;
font-weight: bold;
color: #FFD700;
}
.cost-value {
font-size: 28rpx;
font-weight: bold;
color: #FF6B6B;
}
}
}
.turntable-wrapper {
position: relative;
width: 600rpx;
@ -220,6 +388,20 @@
margin-bottom: 60rpx;
}
.loading-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 600rpx;
.loading-text {
color: white;
font-size: 28rpx;
margin-top: 20rpx;
}
}
.turntable {
width: 100%;
height: 100%;
@ -363,6 +545,7 @@
&.disabled {
opacity: 0.6;
cursor: not-allowed;
background: linear-gradient(145deg, #999, #777);
}
.button-text {
@ -401,7 +584,7 @@
}
.result-prize {
margin-bottom: 40rpx;
margin-bottom: 30rpx;
.result-icon {
font-size: 60rpx;
@ -423,6 +606,19 @@
}
}
.points-change {
margin-bottom: 40rpx;
padding: 20rpx;
background: #f5f5f5;
border-radius: 10rpx;
.change-text {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
}
.confirm-btn {
background: linear-gradient(45deg, #FF6B6B, #FF4757);
color: white;


+ 16
- 35
pages_order/mine/customerService.vue View File

@ -14,16 +14,18 @@
<view class="service-list">
<view
class="service-item"
v-for="(item, index) in serviceList"
v-for="(item, index) in list"
:key="index"
@click="showQRCode(item)"
>
<image class="avatar" :src="item.avatar" mode="aspectFill"></image>
<image class="avatar" :src="item.headImage" mode="aspectFill"></image>
<view class="info">
<view class="name">{{ item.name }}</view>
<view class="contact">{{ item.contact }}</view>
<view class="name">{{ item.nickName }}</view>
<view class="contact">{{ item.phone }}</view>
</view>
<uv-icon name="arrow-right" size="24rpx" color="#999"></uv-icon>
<uv-icon name="arrow-right"
v-if="item.wxCode"
size="24rpx" color="#999"></uv-icon>
</view>
</view>
@ -33,11 +35,11 @@
<uv-popup ref="qrPopup" :round="20" bgColor="#fff">
<view class="qr-popup">
<view class="qr-header">
<view class="title">{{ currentService.name }}</view>
<view class="title">{{ currentService.nickName }}</view>
<view class="subtitle">扫码添加微信</view>
</view>
<view class="qr-content">
<image class="qr-code" :src="currentService.qrCode" mode="aspectFit"></image>
<image class="qr-code" :src="currentService.wxCode" mode="aspectFit"></image>
<view class="qr-tips">长按保存二维码微信扫码添加</view>
</view>
<view class="qr-actions">
@ -49,43 +51,22 @@
</template>
<script>
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
data() {
return {
mixinsListApi: 'getMyService',
currentService: {},
serviceList: [
{
name: '客服小美',
contact: '微信:kefu001',
avatar: '/static/image/avatar/kefu1.jpg',
qrCode: '/static/image/qrcode/kefu1_qr.jpg'
},
{
name: '客服小丽',
contact: '手机:138-8888-8888',
avatar: '/static/image/avatar/kefu2.jpg',
qrCode: '/static/image/qrcode/kefu2_qr.jpg'
},
{
name: '客服小雨',
contact: '微信:kefu003',
avatar: '/static/image/avatar/kefu3.jpg',
qrCode: '/static/image/qrcode/kefu3_qr.jpg'
},
{
name: '客服小花',
contact: '手机:139-9999-9999',
avatar: '/static/image/avatar/kefu4.jpg',
qrCode: '/static/image/qrcode/kefu4_qr.jpg'
}
]
}
},
methods: {
//
showQRCode(service) {
this.currentService = service
this.$refs.qrPopup.open('center')
if(service.wxCode){
this.currentService = service
this.$refs.qrPopup.open('center')
}
}
}
}


+ 3
- 1
pages_order/mine/purse.vue View File

@ -88,7 +88,9 @@
icon: 'none',
})
setTimeout(uni.navigateTo, 800, {
this.$store.commit('getUserInfo')
setTimeout(uni.redirectTo, 800, {
url: '/pages_order/mine/runningWater?status=0'
})
}


Loading…
Cancel
Save