316 lines
7.8 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="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>