bindbox-mini/components/YifanSelector.vue

454 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="choice-grid-container">
<view v-if="loading" class="loading-state">加载中...</view>
<view v-else-if="!choices || choices.length === 0" class="empty-state">暂无可选位置</view>
<view v-else class="grid-wrapper">
<view class="choices-grid">
<view
v-for="(item, index) in choices"
:key="item.id || index"
class="choice-item"
:class="{
'is-sold': item.status === 'sold' || item.is_sold,
'is-selected': isSelected(item),
'is-available': !item.status || item.status === 'available'
}"
@tap="handleSelect(item)"
>
<text class="choice-number">{{ item.number || item.position || index + 1 }}</text>
<view class="choice-status">
<text v-if="item.status === 'sold' || item.is_sold">已售</text>
<text v-else-if="isSelected(item)">已选</text>
<text v-else>可选</text>
</view>
</view>
</view>
<view class="action-bar">
<view class="selection-info" v-if="selectedItems.length > 0">
已选 <text class="highlight">{{ selectedItems.length }}</text> 个位置
</view>
<view class="selection-info" v-else>
请选择位置
</view>
<view class="action-buttons">
<button v-if="selectedItems.length === 0" class="btn-common btn-random" @tap="handleRandomOne">随机一发</button>
<button v-else class="btn-common btn-buy" @tap="handleBuy">去支付</button>
</view>
</view>
</view>
<!-- 支付弹窗 -->
<PaymentPopup
v-model:visible="paymentVisible"
:amount="totalAmount"
:coupons="coupons"
:showCards="false"
@confirm="onPaymentConfirm"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { getIssueChoices, getUserCoupons, joinLottery, createWechatOrder, getLotteryResult } from '@/api/appUser'
import PaymentPopup from '@/components/PaymentPopup.vue'
const props = defineProps({
activityId: { type: [String, Number], required: true },
issueId: { type: [String, Number], required: true },
pricePerDraw: { type: Number, default: 0 } // 单抽价格,用于计算总价
})
const emit = defineEmits(['payment-success'])
const choices = ref([])
const loading = ref(false)
const selectedItems = ref([])
const paymentVisible = ref(false)
// 模拟优惠券和道具卡数据,实际项目中可能需要从接口获取
const coupons = ref([])
const totalAmount = computed(() => {
return (selectedItems.value.length * props.pricePerDraw).toFixed(2)
})
watch(() => props.issueId, (newVal) => {
if (newVal) {
loadChoices()
selectedItems.value = []
}
})
onMounted(() => {
if (props.issueId) {
loadChoices()
}
})
async function loadChoices() {
loading.value = true
try {
const res = await getIssueChoices(props.activityId, props.issueId)
// 处理 { total_slots: 1, available: [1], claimed: [] } 这种格式
if (res && typeof res.total_slots === 'number' && Array.isArray(res.available)) {
const total = res.total_slots
const list = []
// 转换为 Set 提高查找效率,确保类型一致
const availableSet = new Set(res.available.map(v => Number(v)))
for (let i = 1; i <= total; i++) {
const isAvailable = availableSet.has(i)
list.push({
id: i,
number: i,
position: i,
status: isAvailable ? 'available' : 'sold',
is_sold: !isAvailable
})
}
choices.value = list
}
// 兼容旧的/其他的返回结构
else if (Array.isArray(res)) {
choices.value = res
} else if (res && Array.isArray(res.data)) {
choices.value = res.data
} else if (res && Array.isArray(res.choices)) {
choices.value = res.choices
} else {
choices.value = []
}
} catch (error) {
console.error('Failed to load choices:', error)
uni.showToast({ title: '加载位置失败', icon: 'none' })
} finally {
loading.value = false
}
}
function isSelected(item) {
return selectedItems.value.some(i => i.id === item.id || (i.position && i.position === item.position))
}
function handleSelect(item) {
if (item.status === 'sold' || item.is_sold) {
return
}
const index = selectedItems.value.findIndex(i => i.id === item.id || (i.position && i.position === item.position))
if (index > -1) {
selectedItems.value.splice(index, 1)
} else {
selectedItems.value.push(item)
}
}
function handleBuy() {
if (selectedItems.value.length === 0) return
paymentVisible.value = true
fetchCoupons()
}
function handleRandomOne() {
const available = choices.value.filter(item =>
!item.is_sold && item.status !== 'sold' && !isSelected(item)
)
if (available.length === 0) {
uni.showToast({ title: '没有可选位置了', icon: 'none' })
return
}
const randomIndex = Math.floor(Math.random() * available.length)
const randomItem = available[randomIndex]
// 选中该位置
selectedItems.value.push(randomItem)
// 立即弹出支付
paymentVisible.value = true
fetchCoupons()
}
async function fetchCoupons() {
const user_id = uni.getStorageSync('user_id')
if (!user_id) return
try {
const res = await getUserCoupons(user_id, 0, 1, 100)
let list = []
if (Array.isArray(res)) list = res
else if (res && Array.isArray(res.list)) list = res.list
else if (res && Array.isArray(res.data)) list = res.data
coupons.value = list.map((i, idx) => {
const cents = (i.remaining !== undefined && i.remaining !== null) ? Number(i.remaining) : Number(i.amount ?? i.value ?? 0)
const yuan = isNaN(cents) ? 0 : (cents / 100)
return {
id: i.id ?? i.coupon_id ?? String(idx),
name: i.name ?? i.title ?? '优惠券',
amount: Number(yuan).toFixed(2)
}
})
} catch (e) {
console.error('fetchCoupons error', e)
coupons.value = []
}
}
async function onPaymentConfirm(paymentData) {
paymentVisible.value = false
const selectedSlots = selectedItems.value.map(item => item.id || item.position)
if (selectedSlots.length === 0) {
uni.showToast({ title: '未选择位置', icon: 'none' })
return
}
const openid = uni.getStorageSync('openid')
if (!openid) {
uni.showToast({ title: '未获取到OpenID请重新登录', icon: 'none' })
return
}
uni.showLoading({ title: '处理中...' })
try {
// 1. 先调用抽奖接口 (joinLottery),服务器会返回订单号等信息
const payload = {
activity_id: Number(props.activityId),
issue_id: Number(props.issueId),
channel: 'miniapp',
count: selectedSlots.length,
coupon_id: paymentData.coupon ? Number(paymentData.coupon.id) : 0,
slot_index: selectedSlots.map(Number)
}
const joinRes = await joinLottery(payload)
// 假设 joinRes 包含 order_no如果结构不同请调整
const orderNo = joinRes.order_no || joinRes.data?.order_no || joinRes.result?.order_no
if (!orderNo) {
throw new Error('未获取到订单号')
}
// 2. 使用返回的订单号去发起支付
const payRes = await createWechatOrder({
openid: openid,
order_no: orderNo
})
// 调起微信支付
await new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: payRes.timeStamp || payRes.timestamp,
nonceStr: payRes.nonceStr || payRes.noncestr,
package: payRes.package,
signType: payRes.signType || 'MD5',
paySign: payRes.paySign,
success: resolve,
fail: reject
})
})
uni.hideLoading()
uni.showLoading({ title: '查询结果...' })
// 3. 支付成功,查询抽奖结果
const resultRes = await getLotteryResult(orderNo)
console.log('Lottery Result:', resultRes) // 打印结果供查看
uni.hideLoading()
uni.showToast({ title: '支付成功', icon: 'success' })
// 触发支付成功事件
emit('payment-success', {
result: resultRes,
items: selectedItems.value
})
// 清空选择并刷新
selectedItems.value = []
loadChoices()
} catch (e) {
uni.hideLoading()
console.error('Flow failed:', e)
if (e.errMsg && e.errMsg.indexOf('cancel') !== -1) {
uni.showToast({ title: '支付已取消', icon: 'none' })
} else {
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
}
}
}
</script>
<style scoped>
/* ============================================
奇盒潮玩 - 选号组件 (适配高级卡片布局)
============================================ */
/* 容器 - 去除背景,融入父级卡片 */
.choice-grid-container {
padding: 10rpx 0;
background: transparent;
}
/* 加载和空状态 */
.loading-state, .empty-state {
text-align: center;
padding: 60rpx 0;
color: #9CA3AF;
font-size: 26rpx;
}
/* 网格包装 */
.grid-wrapper {
padding-bottom: 160rpx; /* 留出底部操作栏空间 */
}
/* 号码网格 - 8列布局 */
.choices-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 10rpx;
padding: 0;
}
/* 单个号码格子 */
.choice-item {
aspect-ratio: 1;
background: #F9FAFB;
border: 1rpx solid #E5E7EB;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
transition: all 0.2s;
}
.choice-item:active {
transform: scale(0.9);
}
/* 号码文字 */
.choice-number {
font-size: 24rpx;
font-weight: 700;
color: #4B5563;
z-index: 1;
}
/* 状态文字 - 简化为小点或隐藏 */
.choice-status {
display: none;
}
/* ============= 状态样式 ============= */
/* 可选状态 */
.is-available {
background: #F9FAFB;
}
/* 已售状态 */
.is-sold {
background: #F3F4F6;
border-color: #F3F4F6;
opacity: 0.8;
}
.is-sold::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.05);
}
.is-sold .choice-number {
color: #D1D5DB;
text-decoration: line-through;
}
/* 选中状态 - 橙色高亮 */
.is-selected {
background: linear-gradient(135deg, #FF9F43, #FF6B35);
border-color: transparent;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.4);
}
.is-selected .choice-number {
color: #FFFFFF;
}
/* ============= 底部操作栏 ============= */
.action-bar {
position: fixed;
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
padding: 20rpx 30rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
border-radius: 999rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
z-index: 100;
border: 1rpx solid rgba(255, 255, 255, 0.5);
}
/* 选择信息行 */
.selection-info {
font-size: 26rpx;
color: #4B5563;
display: flex;
align-items: center;
}
.highlight {
color: #FF6B35;
font-weight: 800;
font-size: 36rpx;
margin: 0 8rpx;
}
/* 按钮组 */
.action-buttons {
display: flex;
gap: 16rpx;
}
/* 通用按钮样式 */
.btn-common {
height: 72rpx;
line-height: 72rpx;
padding: 0 40rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 600;
margin: 0;
}
/* 购买按钮 */
.btn-buy {
background: linear-gradient(135deg, #FF9F43, #FF6B35) !important;
color: #FFFFFF !important;
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.3);
}
/* 随机按钮 */
.btn-random {
background: #F3F4F6 !important;
color: #4B5563 !important;
}
</style>