bindbox-mini/pages/shop/detail.vue
2025-12-15 11:43:43 +08:00

171 lines
6.2 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>
<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>