316 lines
7.8 KiB
Vue
316 lines
7.8 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">
|
||
<text class="points-val">{{ (detail.points_required ? Math.floor(detail.points_required / 100) : 0) || (detail.price ? Math.floor(detail.price / 100) : 0) }}</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" @tap="onRedeem">立即兑换</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
|
||
}
|
||
|
||
const points = (detail.value.points_required ? Math.floor(detail.value.points_required / 100) : 0) || (detail.value.price ? Math.floor(detail.value.price / 100) : 0)
|
||
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); }
|
||
.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>
|