- 在request.js中添加积分兑换商品API - 在shop页面实现积分兑换功能及UI优化 - 在orders页面优化订单显示逻辑,支持优惠券和道具卡标签 - 在mine页面调整订单导航逻辑,支持跳转至cabinet指定tab - 优化道具卡和优惠券的显示及状态处理
255 lines
6.3 KiB
Vue
255 lines
6.3 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="bg-decoration"></view>
|
||
<view class="loading" v-if="loading">加载中...</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" v-if="detail.points_required">
|
||
<text class="points-val">{{ detail.points_required }}</text>
|
||
<text class="points-unit">积分</text>
|
||
</view>
|
||
<text class="price" v-else>¥{{ formatPrice(detail.price_sale || detail.price) }}</text>
|
||
</view>
|
||
<view class="stock" v-if="detail.stock !== null && detail.stock !== undefined">库存:{{ detail.stock }}</view>
|
||
<view class="desc" v-if="detail.description">{{ detail.description }}</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" v-if="detail.points_required" @tap="onRedeem">立即兑换</view>
|
||
<view class="action-btn buy" v-else @tap="onBuy">立即购买</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)
|
||
|
||
function formatPrice(p) {
|
||
if (p === undefined || p === null) return '0.00'
|
||
return (Number(p) / 100).toFixed(2)
|
||
}
|
||
|
||
async function fetchDetail(id) {
|
||
loading.value = true
|
||
try {
|
||
const res = await getProductDetail(id)
|
||
detail.value = res || {}
|
||
} catch (e) {
|
||
detail.value = {}
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function onBuy() {
|
||
uni.showToast({ title: '暂未开放购买', icon: 'none' })
|
||
}
|
||
|
||
async function onRedeem() {
|
||
const p = detail.value
|
||
if (!p || !p.id) 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
|
||
}
|
||
|
||
uni.showModal({
|
||
title: '确认兑换',
|
||
content: `是否消耗 ${p.points_required} 积分兑换 ${p.title}?`,
|
||
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.showToast({ title: '兑换成功', icon: 'success' })
|
||
|
||
// Refresh detail
|
||
setTimeout(() => {
|
||
fetchDetail(p.id)
|
||
}, 1500)
|
||
} catch (e) {
|
||
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
.loading, .empty {
|
||
text-align: center;
|
||
padding: 120rpx 40rpx;
|
||
color: $text-secondary;
|
||
font-size: $font-md;
|
||
}
|
||
|
||
.detail-wrap {
|
||
padding-bottom: 40rpx;
|
||
animation: fadeInUp 0.4s ease-out;
|
||
}
|
||
|
||
.main-image {
|
||
width: 100%;
|
||
height: 750rpx; /* Square aspect ratio */
|
||
display: block;
|
||
background: $bg-secondary;
|
||
box-shadow: $shadow-sm;
|
||
}
|
||
|
||
.info-card {
|
||
background: #fff;
|
||
border-radius: $radius-xl $radius-xl 0 0;
|
||
padding: $spacing-xl;
|
||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
||
position: relative;
|
||
z-index: 2;
|
||
margin-top: -40rpx; /* Slight overlap */
|
||
min-height: 50vh;
|
||
}
|
||
|
||
.title {
|
||
font-size: $font-xl;
|
||
font-weight: 800;
|
||
color: $text-main;
|
||
margin-bottom: $spacing-md;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.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: $bg-secondary;
|
||
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;
|
||
}
|
||
}
|
||
|
||
.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: 10;
|
||
}
|
||
.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;
|
||
}
|
||
.action-btn.redeem { background: linear-gradient(135deg, #FFB74D, #FF9800); box-shadow: 0 8rpx 20rpx rgba(255, 152, 0, 0.3); }
|
||
.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>
|