HaniBlindBox/honey_box/pages/mall/detail.vue
2026-03-14 15:43:23 +08:00

644 lines
17 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="mall-detail">
<!-- 顶部导航 -->
<uni-nav-bar left-icon="left" title="商品详情" color="#000000" backgroundColor="#fff" :fixed="true"
:statusBar="true" :border="false" @clickLeft="goBack"></uni-nav-bar>
<view class="detail-content-wrap">
<!-- Banner图 -->
<view class="goods-banner" v-if="goodsInfo.imgurl_banner">
<image :src="goodsInfo.imgurl_banner" mode="widthFix" class="banner-img"></image>
</view>
<!-- 价格区域 -->
<view class="price-section">
<view class="price-left">
<text class="price-symbol">¥</text>
<text class="price-value">{{ goodsInfo.price }}</text>
</view>
<text class="price-coupon" v-if="orderData && orderData.goods_extend && orderData.goods_extend.pay_currency2 == 1">
{{ goodsInfo.price * 100 }}{{ $config.getAppSetting('currency2_name') }}
</text>
</view>
<!-- 商品标题 -->
<view class="title-section">
<text class="goods-title">{{ goodsInfo.title }}</text>
</view>
<!-- 信息区域 -->
<view class="info-section">
<view class="info-row">
<text class="info-label">库存</text>
<text class="info-value info-value-inline">{{ goodsInfo.goodslist_surplus_stock != null ? goodsInfo.goodslist_surplus_stock : (goodsInfo.stock || 0) }}</text>
</view>
</view>
<view class="info-section" v-if="orderData && orderData.goods_extend && orderData.goods_extend.pay_currency2 == 1">
<view class="info-row">
<text class="info-label">{{ $config.getAppSetting('currency2_name') }}{{ goodsInfo.price * 100 }}</text>
<text class="info-value info-sub info-value-inline">(剩余{{ orderData.score || 0 }})</text>
</view>
</view>
<!-- 售前售后须知 -->
<view class="info-section" @click="showRule = true">
<view class="info-row">
<text class="info-label">售前·售后须知</text>
<text class="info-arrow"></text>
</view>
</view>
<!-- 商品详情图 -->
<view class="detail-section" v-if="goodsInfo.imgurl_detail">
<text class="detail-title">商品详情</text>
<view class="detail-content">
<image :src="goodsInfo.imgurl_detail" mode="widthFix" style="width: 100%;"></image>
</view>
</view>
<!-- 底部占位 -->
<view style="height: 180rpx;"></view>
</view>
<!-- 底部购买按钮 -->
<view class="bottom-bar">
<view class="buy-btn" @click="onBuy">
<text v-if="orderData && orderData.goods_extend && orderData.goods_extend.pay_currency2 == 1">
{{ goodsInfo.price * 100 }}{{ $config.getAppSetting('currency2_name') }} 立即兑换
</text>
<text v-else>¥{{ goodsInfo.price }} 立即购买</text>
</view>
</view>
<!-- 售前售后弹窗 -->
<uni-popup ref="rulePop" type="bottom">
<view class="rule-popup">
<view class="rule-popup-hd">
<text>售前·售后须知</text>
<view @click="$refs.rulePop.close()" class="rule-close">✕</view>
</view>
<scroll-view scroll-y class="rule-popup-body">
<rich-text :nodes="sendRuleData"></rich-text>
</scroll-view>
</view>
</uni-popup>
<!-- 确认订单弹窗 -->
<uni-popup ref="orderPop" type="bottom">
<view v-if="orderData" class="order-popup">
<view class="order-popup-hd">
<view style="width: 24rpx;"></view>
<text>确认订单</text>
<view class="rule-close" @click="$refs.orderPop.close()">✕</view>
</view>
<view class="order-card">
<image :src="orderData.goods.imgurl"
style="width: 190rpx; height: 190rpx; border-radius: 10rpx; flex-shrink: 0;" mode="aspectFill"></image>
<view class="order-card-info">
<text class="hang1" style="font-size: 28rpx; color: #333;">{{ orderData.goods.title }}</text>
<view class="order-card-price">
<text style="color: #333; font-size: 28rpx;">¥{{ orderData.goods.price }}</text>
<text style="color: #999; font-size: 24rpx;">×{{ orderData.goods.prize_num }}</text>
</view>
</view>
</view>
<!-- 支付方式 -->
<view class="pay-methods">
<view class="pay-method-item" @click="zhifu = 0"
v-if="orderData.goods_extend && orderData.goods_extend.pay_wechat == 1">
<text style="color: #333;">微信支付</text>
<image :src="zhifu == 0 ? $img1('common/check_act.png') : $img1('common/check.png')"
style="width: 32rpx; height: 32rpx;"></image>
</view>
<view class="pay-method-item" @click="zhifu = 1"
v-if="orderData.goods_extend && orderData.goods_extend.pay_currency2 == 1">
<text style="color: #333;">{{ $config.getAppSetting('currency2_name') }}
{{ orderData.goods.price * 100 }} (剩余:{{ orderData.score || 0 }})</text>
<image :src="zhifu == 1 ? $img1('common/check_act.png') : $img1('common/check.png')"
style="width: 32rpx; height: 32rpx;"></image>
</view>
</view>
<!-- 规则 -->
<view class="order-rule" v-if="sendRuleData">
<scroll-view scroll-y style="max-height: 160rpx;">
<rich-text :nodes="sendRuleData" style="font-size: 20rpx; color: #999;"></rich-text>
</scroll-view>
</view>
<!-- 协议 -->
<view class="order-agree" @click="isAgree = !isAgree">
<image :src="isAgree ? $img1('common/check_act.png') : $img1('common/check.png')"
style="width: 32rpx; height: 32rpx; margin-right: 10rpx;"></image>
<text>我已满18岁,阅读并同意</text>
<text style="color: #1e88e5;" @click.stop="$c.to({ url: '/pages/guize/guize?type=4' })">《用户协议》</text>
<text style="color: #1e88e5;" @click.stop="$c.to({ url: '/pages/guize/guize?type=5' })">《隐私政策》</text>
</view>
<!-- 支付按钮 -->
<view class="order-pay-btn" @click="confirmPay">
<text v-if="zhifu == 0">¥{{ orderData.price }}</text>
<text v-if="zhifu == 1">{{ orderData.price * 100 }}{{ $config.getAppSetting('currency2_name') }}</text>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { calcMallOrderMoney, createOrder, createMallOrder } from '@/common/server/order.js';
import { getGoodsDetail } from '@/common/server/goods.js';
import RequestManager from '@/common/request.js';
export default {
data() {
return {
goodsId: '',
goodsInfo: {},
orderData: null,
sendRuleData: '',
zhifu: 0,
isAgree: true,
showRule: false
};
},
onLoad(options) {
if (options.goods_id) {
this.goodsId = options.goods_id;
// 从列表页传来的图片作为兜底
if (options.imgurl) {
this.goodsInfo = { imgurl: decodeURIComponent(options.imgurl) };
}
this.loadDetail();
}
this.$c.getRule(10).then(res => {
if (res.status == 1) this.sendRuleData = res.data;
});
},
onShow() {
// 登录回来后,如果还没有支付信息,重新加载
const token = uni.getStorageSync('token');
if (token && !this.orderData && this.goodsId) {
this.loadDetail();
}
},
watch: {
showRule(val) {
if (val) {
this.$refs.rulePop.open();
this.showRule = false;
}
}
},
methods: {
goBack() {
uni.navigateBack();
},
async loadDetail() {
// 1. 先用 goods_detail 获取商品基本信息(不需要登录)
try {
const detailRes = await getGoodsDetail(this.goodsId);
if (detailRes.status == 1 && detailRes.data) {
// goods_detail 返回 { data: { goods: {...}, goodslist: [...] } }
this.goodsInfo = detailRes.data.goods || detailRes.data;
// [调试日志] 详情页 - goods_detail 返回的库存数据
const g = this.goodsInfo;
console.log(`[商城详情] goods_detail: id=${g.id} stock=${g.stock} sale_stock=${g.sale_stock} goodslist_stock=${g.goodslist_stock} goodslist_surplus_stock=${g.goodslist_surplus_stock} imgurl_banner=${g.imgurl_banner}`);
}
} catch (e) {
console.log('获取商品详情失败', e);
}
// 2. 已登录时,再调 calcMallOrderMoney 获取支付信息
const token = uni.getStorageSync('token');
if (token) {
try {
const res = await calcMallOrderMoney({
prize_num: 1,
goods_id: this.goodsId,
num: 1,
});
if (res.status == 1) {
this.orderData = res.data;
// 用订单接口的数据补充商品信息(含 imgurl_detail、goods_describe 等)
if (res.data.goods) {
// [调试日志] 详情页 - calcMallOrderMoney 返回的商品数据
const mg = res.data.goods;
console.log(`[商城详情] mall_ordermoney: id=${mg.id} stock=${mg.stock} sale_stock=${mg.sale_stock} goodslist_surplus_stock=${mg.goodslist_surplus_stock} imgurl_banner=${mg.imgurl_banner}`);
this.goodsInfo = { ...this.goodsInfo, ...res.data.goods };
// [调试日志] 合并后的最终数据
const fg = this.goodsInfo;
console.log(`[商城详情] 合并后: stock=${fg.stock} sale_stock=${fg.sale_stock} goodslist_surplus_stock=${fg.goodslist_surplus_stock} imgurl_banner=${fg.imgurl_banner}`);
}
const ext = res.data.goods_extend || {};
if (ext.pay_wechat == 1) {
this.zhifu = 0;
} else if (ext.pay_currency2 == 1) {
this.zhifu = 1;
}
}
} catch (e) {
console.log('获取支付信息失败', e);
}
}
},
// 静默刷新详情,不显示"加载中"提示
async silentLoadDetail() {
try {
const detailRes = await RequestManager.get('/goods_detail', { goods_id: this.goodsId }, false);
if (detailRes.status == 1 && detailRes.data) {
this.goodsInfo = detailRes.data.goods || detailRes.data;
}
} catch (e) {
console.log('静默刷新商品详情失败', e);
}
const token = uni.getStorageSync('token');
if (token) {
try {
const res = await RequestManager.post('/mall_ordermoney', {
prize_num: 1,
goods_id: this.goodsId,
num: 1,
}, false);
if (res.status == 1) {
this.orderData = res.data;
if (res.data.goods) {
this.goodsInfo = { ...this.goodsInfo, ...res.data.goods };
}
}
} catch (e) {
console.log('静默刷新支付信息失败', e);
}
}
},
onBuy() {
const token = uni.getStorageSync('token');
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' });
setTimeout(() => {
uni.navigateTo({ url: '/pages/user/login' });
}, 300);
return;
}
// 检查库存(商城赏用奖品维度的剩余库存)
const remainStock = this.goodsInfo.goodslist_surplus_stock != null
? this.goodsInfo.goodslist_surplus_stock
: (this.goodsInfo.stock || 0) - (this.goodsInfo.sale_stock || 0);
// [调试日志] 兑换按钮 - 库存校验
console.log(`[商城详情] 兑换校验: goodslist_surplus_stock=${this.goodsInfo.goodslist_surplus_stock} stock=${this.goodsInfo.stock} sale_stock=${this.goodsInfo.sale_stock} remainStock=${remainStock}`);
if (remainStock <= 0) {
this.$c.msg('库存不足,兑换失败');
return;
}
// 登录后如果还没加载支付信息,先加载
if (!this.orderData) {
this.loadDetail().then(() => {
if (this.orderData) {
this.$refs.orderPop.open();
}
});
return;
}
this.$refs.orderPop.open();
},
async confirmPay() {
if (!this.isAgree) {
this.$c.msg('请先同意用户协议');
return;
}
console.log(`[商城订单] confirmPay: zhifu=${this.zhifu} price=${this.goodsInfo.price} score=${this.orderData?.score}`);
if (this.zhifu == 0) {
await this.payByWechat();
} else {
await this.payByCurrency();
}
},
async payByWechat() {
const data = {
goods_id: this.goodsId,
prize_num: 1,
num: 1,
use_money_is: 2,
use_integral_is: 2,
coupon_id: '',
use_money2_is: 2
};
console.log(`[商城订单] payByWechat 请求参数:`, JSON.stringify(data));
const res = await createOrder(data);
console.log(`[商城订单] payByWechat 响应: status=${res.status} msg=${res.msg} data.status=${res.data?.status}`);
if (res.status == 1) {
this.$refs.orderPop.close();
if (res.data.status == 1) {
const status = await this.$platform.pay({ data: res.data.res }, this);
console.log(`[商城订单] 微信支付结果: ${status}`);
if (status == 'success') {
this.$c.msg('购买成功');
this.loadDetail();
}
} else {
this.$c.toast({ title: res.msg, duration: 500, success: () => this.loadDetail() });
}
}
},
async payByCurrency() {
const pri = this.goodsInfo.price * 100;
console.log(`[商城订单] payByCurrency: price=${this.goodsInfo.price} 需要=${pri} 剩余score=${this.orderData.score}`);
if ((this.orderData.score || 0) < pri) {
this.$c.msg(this.$config.getAppSetting('currency2_name') + '不足');
return;
}
// 再次检查库存(防止并发购买)
const remainStock = this.goodsInfo.goodslist_surplus_stock != null
? this.goodsInfo.goodslist_surplus_stock
: (this.goodsInfo.stock || 0) - (this.goodsInfo.sale_stock || 0);
console.log(`[商城订单] payByCurrency 库存校验: goodslist_surplus_stock=${this.goodsInfo.goodslist_surplus_stock} remainStock=${remainStock}`);
if (remainStock <= 0) {
this.$c.msg('库存不足,兑换失败');
return;
}
console.log(`[商城订单] createMallOrder 请求: goods_id=${this.goodsId}`);
const res = await createMallOrder({ goods_id: this.goodsId, prize_num: 1 });
console.log(`[商城订单] createMallOrder 响应: status=${res.status} msg=${res.msg} data.status=${res.data?.status}`);
if (res.status == 1) {
this.$refs.orderPop.close();
if (res.data.status == 1) {
const status = await this.$platform.pay({ data: res.data.res }, this);
if (status == 'success') {
this.$c.msg('兑换成功');
// 静默刷新,避免出现"加载中"提示
setTimeout(() => {
this.silentLoadDetail();
}, 1600);
}
} else {
// 兑换成功(积分/货币直接扣除,无需微信支付)
this.$c.msg('兑换成功');
// 静默刷新,避免出现"加载中"提示
setTimeout(() => {
this.silentLoadDetail();
}, 1600);
}
}
}
}
};
</script>
<style lang="scss" scoped>
.mall-detail {
min-height: 100vh;
background: #f5f5f5;
}
.goods-banner {
width: 100%;
background: #fff;
.banner-img {
width: 100%;
}
}
.price-section {
padding: 30rpx 30rpx 10rpx;
background: #fff;
display: flex;
justify-content: space-between;
align-items: baseline;
.price-left {
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: 28rpx;
color: #ff4d4f;
font-weight: 600;
}
.price-value {
font-size: 52rpx;
color: #ff4d4f;
font-weight: 600;
}
.price-coupon {
font-size: 26rpx;
color: #f5a623;
font-weight: 500;
}
}
.title-section {
padding: 10rpx 30rpx 30rpx;
background: #fff;
.goods-title {
font-size: 30rpx;
color: #333;
line-height: 1.5;
}
}
.info-section {
margin-top: 2rpx;
padding: 24rpx 30rpx;
background: #fff;
.info-row {
display: flex;
align-items: center;
}
.info-label {
font-size: 28rpx;
color: #333;
}
.info-value {
font-size: 28rpx;
color: #333;
}
.info-value-inline {
margin-left: 20rpx;
}
.info-sub {
color: #999;
font-size: 24rpx;
}
.info-arrow {
font-size: 36rpx;
color: #999;
margin-left: auto;
}
}
.detail-section {
margin-top: 20rpx;
background: #fff;
padding: 30rpx;
.detail-title {
display: block;
text-align: center;
font-size: 28rpx;
color: #999;
margin-bottom: 30rpx;
}
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
background: #fff;
.buy-btn {
width: 694rpx;
height: 86rpx;
background: #03D8F4;
border-radius: 108rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 500;
color: #fff;
}
}
.rule-popup {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
max-height: 70vh;
.rule-popup-hd {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
font-size: 32rpx;
font-weight: 500;
}
.rule-popup-body {
padding: 0 30rpx 60rpx;
max-height: 50vh;
font-size: 24rpx;
color: #666;
}
}
.rule-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #999;
}
.order-popup {
background: #f7f7f7;
border-radius: 24rpx 24rpx 0 0;
padding: 0 30rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
.order-popup-hd {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
font-size: 32rpx;
font-weight: 500;
}
}
.order-card {
display: flex;
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
.order-card-info {
flex: 1;
min-width: 0;
margin-left: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.order-card-price {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.pay-methods {
margin-top: 20rpx;
background: #fff;
border-radius: 20rpx;
.pay-method-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
font-size: 28rpx;
}
}
.order-rule {
margin-top: 20rpx;
padding: 20rpx;
background: #F9F8E1;
border-radius: 20rpx;
}
.order-agree {
display: flex;
align-items: center;
margin-top: 20rpx;
font-size: 20rpx;
color: #999;
}
.order-pay-btn {
margin-top: 30rpx;
width: 100%;
height: 84rpx;
background: #5CE5D5;
border-radius: 42rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 500;
color: #333;
}
</style>