380 lines
9.7 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="page">
<view class="bg-decoration"></view>
<view class="loading" v-if="loading">加载中...</view>
<view v-else-if="isOutOfStock" class="empty">商品库存不足由于市场价格存在波动请联系客服核实价格和补充库存</view>
<view v-else-if="detail.id" class="detail-wrap">
<image v-if="detail.main_image" class="main-image" :src="detail.main_image" mode="widthFix" />
<view class="info-card">
<view class="title">{{ detail.title || detail.name || '-' }}</view>
<view class="price-row">
<view class="points-wrap">
<text class="points-val">{{ formatPoints( detail.price) }}</text>
<text class="points-unit">积分</text>
</view>
</view>
<view class="stock" v-if="detail.stock !== null && detail.stock !== undefined">库存{{ detail.stock }}</view>
<view class="desc" v-if="detail.description">
<rich-text :nodes="detail.description"></rich-text>
</view>
</view>
</view>
<view v-else class="empty">商品不存在</view>
<!-- Action Bar Moved Outside info-card -->
<view class="action-bar-placeholder" v-if="detail.id"></view>
<view class="action-bar" v-if="detail.id">
<view
class="action-btn redeem"
:class="{ disabled: detail.stock === 0 }"
@tap="onRedeem"
>
{{ detail.stock === 0 ? '已售罄' : '立即兑换' }}
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getProductDetail } from '../../api/appUser'
import { redeemProductByPoints } from '../../utils/request.js'
const detail = ref({})
const loading = ref(false)
const isOutOfStock = ref(false)
function formatPrice(p) {
if (p === undefined || p === null) return '0.00'
return (Number(p) / 100).toFixed(2)
}
// 格式化积分显示 - 不四舍五入,保留两位小数
function formatPoints(value) {
if (value === undefined || value === null) return '0.00'
const num = Number(value)
if (isNaN(num)) return '0.00'
// 价格字段单位是分,如 1250 = 12.50积分
// 除以100得到显示值
const finalValue = num / 100
// 使用 Math.floor 避免四舍五入,保留两位小数
return String(Math.floor(finalValue * 100) / 100).replace(/(\.\d)$/, '$10')
}
async function fetchDetail(id) {
loading.value = true
isOutOfStock.value = false
try {
const res = await getProductDetail(id)
detail.value = res || {}
console.log(detail.value);
if (detail.value.code === 20002 || detail.value.message === '商品缺货') {
// 商品缺货时标记状态,这样页面会显示缺货提示而不是"商品不存在"
// request.js 中已经会弹出提示窗口
isOutOfStock.value = true
console.log('[商品详情] 商品缺货')
}
} catch (e) {
// 检查是否是商品缺货错误 (code: 20002)
const errorCode = e?.data?.code || e?.code
const errorMessage = e?.data?.message || e?.message || e?.msg
if (errorCode === 20002 || errorMessage === '商品缺货') {
// 商品缺货时标记状态,这样页面会显示缺货提示而不是"商品不存在"
// request.js 中已经会弹出提示窗口
isOutOfStock.value = true
console.log('[商品详情] 商品缺货')
} else {
// 其他错误才清空并显示"商品不存在"
detail.value = {}
console.log('[商品详情] 错误信息:', e)
uni.showToast({ title: errorMessage || '加载失败', icon: 'none' })
}
} finally {
loading.value = false
}
}
function onBuy() {
uni.showToast({ title: '暂未开放购买', icon: 'none' })
}
async function onRedeem() {
const p = detail.value
if (!p || !p.id) return
// 检查商品库存
if (p.stock === 0) {
uni.showModal({
title: '商品已售罄',
content: '该商品库存不足,请联系客服处理',
showCancel: false
})
return
}
const token = uni.getStorageSync('token')
if (!token) {
uni.showModal({
title: '提示',
content: '请先登录',
confirmText: '去登录',
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
})
return
}
const points = formatPoints(p.price)
uni.showModal({
title: '确认兑换',
content: `是否消耗 ${points} 积分兑换 ${p.title || p.name}`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '兑换中...' })
try {
const userId = uni.getStorageSync('user_id')
if (!userId) throw new Error('用户ID不存在')
await redeemProductByPoints(userId, p.id, 1)
uni.hideLoading()
uni.showModal({
title: '兑换成功',
content: `您已成功兑换 ${p.title || p.name}`,
showCancel: false,
success: () => {
fetchDetail(p.id)
}
})
} catch (e) {
uni.hideLoading()
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
}
}
}
})
}
onLoad((opts) => {
const id = opts && opts.id
if (id) fetchDetail(id)
})
</script>
<style lang="scss" scoped>
/* ============================================
柯大鸭潮玩 - 商品详情页
============================================ */
.page {
min-height: 100vh;
background: $bg-page;
padding-bottom: env(safe-area-inset-bottom);
position: relative;
overflow: hidden;
}
/* 背景装饰 - 漂浮光球 (与各主要页面统一) */
.bg-decoration {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: -100rpx; right: -100rpx;
width: 600rpx; height: 600rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.15) 0%, transparent 70%);
filter: blur(60rpx);
border-radius: 50%;
opacity: 0.8;
animation: float 10s ease-in-out infinite;
}
&::after {
content: '';
position: absolute;
top: 200rpx; left: -200rpx;
width: 500rpx; height: 500rpx;
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
filter: blur(50rpx);
border-radius: 50%;
opacity: 0.6;
animation: float 15s ease-in-out infinite reverse;
}
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 50rpx); }
}
.loading, .empty {
text-align: center;
padding: 120rpx 40rpx;
color: $text-secondary;
font-size: $font-md;
position: relative;
z-index: 10;
}
.detail-wrap {
padding-bottom: 40rpx;
animation: fadeInUp 0.4s ease-out;
position: relative;
z-index: 1;
}
.main-image {
width: 100%;
height: 750rpx;
display: block;
background: $bg-secondary;
}
.info-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20rpx);
border-radius: $radius-xl $radius-xl 0 0;
padding: $spacing-xl;
box-shadow: 0 -8rpx 32rpx rgba(0,0,0,0.05);
position: relative;
z-index: 2;
margin-top: -40rpx;
min-height: 50vh;
border-top: 1px solid rgba(255, 255, 255, 0.6);
border-left: 1px solid rgba(255, 255, 255, 0.6);
border-right: 1px solid rgba(255, 255, 255, 0.6);
}
.title {
font-size: $font-xl;
font-weight: 800;
color: $text-main;
margin-bottom: $spacing-md;
line-height: 1.4;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
}
.price-row {
display: flex;
align-items: baseline;
gap: $spacing-sm;
margin-bottom: $spacing-lg;
}
.price {
font-size: $font-xxl;
font-weight: 900;
color: $brand-primary;
font-family: 'DIN Alternate', sans-serif;
&::before {
content: '¥';
font-size: $font-md;
margin-right: 4rpx;
}
}
.points-wrap {
display: flex; align-items: baseline;
}
.points-val {
font-size: 48rpx;
font-weight: 900;
color: #FF9800;
font-family: 'DIN Alternate', sans-serif;
}
.points-unit {
font-size: 24rpx;
color: #FF9800;
margin-left: 6rpx;
font-weight: 600;
}
.stock {
font-size: $font-sm;
color: $text-secondary;
margin-bottom: $spacing-lg;
background: rgba(0,0,0,0.05);
display: inline-block;
padding: 6rpx $spacing-md;
border-radius: $radius-sm;
}
.desc {
font-size: $font-lg;
color: $text-main;
line-height: 1.8;
padding-top: $spacing-lg;
border-top: 1rpx dashed $border-color-light;
&::before {
content: '商品详情';
display: block;
font-size: $font-md;
color: $text-secondary;
margin-bottom: $spacing-sm;
font-weight: 700;
}
:deep(img) {
max-width: 100%;
height: auto;
display: block;
}
}
.action-bar-placeholder { height: 120rpx; }
.action-bar {
position: fixed;
bottom: 0; left: 0; right: 0;
padding: 20rpx 40rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
display: flex;
justify-content: flex-end;
z-index: 100;
}
.action-btn {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 700;
color: #fff;
transition: transform 0.2s;
&:active { transform: scale(0.96); }
}
.action-btn.redeem {
background: linear-gradient(135deg, #FFB74D, #FF9800);
box-shadow: 0 8rpx 20rpx rgba(255, 152, 0, 0.3);
&.disabled {
background: #ccc;
box-shadow: none;
color: #999;
}
}
.action-btn.buy { background: linear-gradient(135deg, #FF6B6B, #FF3B30); box-shadow: 0 8rpx 20rpx rgba(255, 59, 48, 0.3); }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(40rpx); }
to { opacity: 1; transform: translateY(0); }
}
</style>