171 lines
6.2 KiB
Vue
171 lines
6.2 KiB
Vue
<template>
|
||
<scroll-view class="page" scroll-y>
|
||
<view class="banner" v-if="product.image">
|
||
<image class="banner-img" :src="product.image" mode="widthFix" />
|
||
<view class="banner-badge" v-if="requiredPoints > 0">{{ requiredPoints }}积分</view>
|
||
</view>
|
||
<view class="header">
|
||
<view class="title">{{ product.title || product.name || '-' }}</view>
|
||
<view class="meta" v-if="requiredPoints > 0">所需积分:{{ requiredPoints }}</view>
|
||
<view class="meta" v-if="userPoints !== null">我的积分:{{ userPoints }}</view>
|
||
</view>
|
||
<view class="desc" v-if="product.description">{{ product.description }}</view>
|
||
<view class="actions">
|
||
<view class="qty">
|
||
<text class="label">数量</text>
|
||
<view class="stepper">
|
||
<text class="btn" @tap="changeQty(-1)">-</text>
|
||
<text class="num">{{ quantity }}</text>
|
||
<text class="btn" @tap="changeQty(1)">+</text>
|
||
</view>
|
||
</view>
|
||
<button class="redeem-btn" type="primary" @tap="onRedeem" :disabled="redeeming || !canRedeem">积分兑换</button>
|
||
<view class="hint" v-if="requiredPoints > 0">共需积分:{{ totalRequired }}</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { getProductDetail, getPointsBalance, redeemProductByPoints } from '@/api/appUser'
|
||
|
||
const productId = ref('')
|
||
const product = ref({})
|
||
const quantity = ref(1)
|
||
const userPoints = ref(null)
|
||
const redeeming = ref(false)
|
||
|
||
const requiredPoints = computed(() => {
|
||
const p = product.value || {}
|
||
const n = Number(p.price)
|
||
return isNaN(n) ? 0 : n
|
||
})
|
||
const totalRequired = computed(() => requiredPoints.value * quantity.value)
|
||
const canRedeem = computed(() => {
|
||
const up = Number(userPoints.value)
|
||
const need = Number(totalRequired.value)
|
||
return requiredPoints.value > 0 && !isNaN(up) && up >= need
|
||
})
|
||
|
||
function changeQty(delta) {
|
||
const next = quantity.value + delta
|
||
quantity.value = next < 1 ? 1 : next
|
||
}
|
||
|
||
async function fetchDetail(id) {
|
||
try {
|
||
const res = await getProductDetail(id)
|
||
const data = res && (res.data || res.item || res.product) ? (res.data || res.item || res.product) : res
|
||
const img = data && (data.main_image || data.image || data.img || data.pic || '')
|
||
const title = data && (data.title || data.name || data.product_name || '')
|
||
const price = data && (data.price ?? data.points_required ?? data.points ?? data.integral ?? null)
|
||
const description = data && (data.description ?? data.desc ?? data.rules ?? '')
|
||
const albumRaw = data && data.album
|
||
const album = normalizeAlbum(albumRaw)
|
||
const first = album[0] || img
|
||
product.value = { image: first, album, title, price, description, ...data }
|
||
} catch (e) {
|
||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function fetchMyPoints() {
|
||
try {
|
||
const uid = uni.getStorageSync('user_id')
|
||
const b = await getPointsBalance(uid)
|
||
const balance = b && b.balance !== undefined ? b.balance : b
|
||
userPoints.value = Number(balance) || 0
|
||
} catch (e) { userPoints.value = 0 }
|
||
}
|
||
|
||
async function onRedeem() {
|
||
const token = uni.getStorageSync('token')
|
||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||
if (!token || !phoneBound) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请先登录并绑定手机号',
|
||
confirmText: '去登录',
|
||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||
})
|
||
return
|
||
}
|
||
const need = requiredPoints.value * quantity.value
|
||
if (!requiredPoints.value || need <= 0) {
|
||
uni.showToast({ title: '该商品未设置积分', icon: 'none' })
|
||
return
|
||
}
|
||
if (userPoints.value === null) {
|
||
await fetchMyPoints()
|
||
}
|
||
if (Number(userPoints.value) < Number(need)) {
|
||
uni.showToast({ title: '积分不足', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
redeeming.value = true
|
||
const uid = uni.getStorageSync('user_id')
|
||
await redeemProductByPoints(uid, productId.value, quantity.value)
|
||
uni.showToast({ title: '兑换成功', icon: 'success' })
|
||
try {
|
||
const b = await getPointsBalance(uid)
|
||
const balance = b && b.balance !== undefined ? b.balance : b
|
||
userPoints.value = Number(balance) || 0
|
||
} catch (_) {}
|
||
} catch (e) {
|
||
uni.showToast({ title: e && (e.message || e.errMsg) || '兑换失败', icon: 'none' })
|
||
} finally {
|
||
redeeming.value = false
|
||
}
|
||
}
|
||
|
||
onLoad((opts) => {
|
||
const id = (opts && (opts.id || opts.product_id)) || ''
|
||
productId.value = id
|
||
if (id) fetchDetail(id)
|
||
fetchMyPoints()
|
||
})
|
||
|
||
function normalizeAlbum(x) {
|
||
const result = []
|
||
try {
|
||
if (!x) return result
|
||
if (Array.isArray(x)) {
|
||
return x.map(s => String(s || '').trim()).filter(Boolean)
|
||
}
|
||
const s = String(x || '').trim()
|
||
if (!s) return result
|
||
if (s.startsWith('[') && s.endsWith(']')) {
|
||
const arr = JSON.parse(s)
|
||
if (Array.isArray(arr)) return arr.map(u => String(u || '').trim()).filter(Boolean)
|
||
}
|
||
if (s.includes(',')) {
|
||
return s.split(',').map(u => u.trim()).filter(Boolean)
|
||
}
|
||
return [s]
|
||
} catch (_) {
|
||
return result
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page { min-height: 100vh; padding-bottom: 120rpx; background: #F8F8F8 }
|
||
.banner { position: relative; padding: 0 }
|
||
.banner-img { width: 100%; display: block }
|
||
.banner-badge { position: absolute; right: 16rpx; bottom: 16rpx; background: rgba(0,0,0,0.7); color: #fff; padding: 8rpx 16rpx; border-radius: 24rpx; font-size: 24rpx }
|
||
.header { padding: 24rpx; background: #fff }
|
||
.title { font-size: 36rpx; font-weight: 700; color: #333 }
|
||
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
|
||
.desc { background: #fff; padding: 0 24rpx 24rpx; font-size: 26rpx; color: #444 }
|
||
.actions { margin-top: 12rpx; padding: 24rpx; background: #fff }
|
||
.qty { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx }
|
||
.label { font-size: 28rpx }
|
||
.stepper { display: flex; align-items: center; }
|
||
.btn { width: 56rpx; height: 56rpx; line-height: 56rpx; text-align: center; background: #f4f4f4; border-radius: 12rpx }
|
||
.num { width: 80rpx; text-align: center; font-size: 28rpx }
|
||
.redeem-btn { margin-top: 8rpx }
|
||
.hint { margin-top: 12rpx; font-size: 24rpx; color: #999 }
|
||
</style>
|