HaniBlindBox/honey_box/pages/mall/detail.vue
2026-03-14 11:25:19 +08:00

608 lines
15 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">
<!-- 商品主图 -->
<view class="goods-banner">
<image :src="goodsInfo.imgurl_banner || goodsInfo.imgurl" mode="widthFix" class="banner-img"></image>
</view>
<!-- 价格区域 -->
<view class="price-section">
<text class="price-symbol">¥</text>
<text class="price-value">{{ goodsInfo.price }}</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">{{ 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">(剩余:{{ 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 class="detail-section" v-if="goodsInfo.goods_describe">
<text class="detail-title" v-if="!goodsInfo.imgurl_detail">商品详情</text>
<view class="detail-content">
<rich-text :nodes="goodsInfo.goods_describe"></rich-text>
</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;
}
} 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) {
this.goodsInfo = { ...this.goodsInfo, ...res.data.goods };
}
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);
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;
}
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
};
const res = await createOrder(data);
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('购买成功');
this.loadDetail();
}
} else {
this.$c.toast({ title: res.msg, duration: 500, success: () => this.loadDetail() });
}
}
},
async payByCurrency() {
const pri = this.goodsInfo.price * 100;
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);
if (remainStock <= 0) {
this.$c.msg('库存不足,兑换失败');
return;
}
const res = await createMallOrder({ goods_id: this.goodsId, prize_num: 1 });
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;
.price-symbol {
font-size: 28rpx;
color: #ff4d4f;
font-weight: 600;
}
.price-value {
font-size: 52rpx;
color: #ff4d4f;
font-weight: 600;
}
}
.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;
justify-content: space-between;
align-items: center;
}
.info-label {
font-size: 28rpx;
color: #333;
}
.info-value {
font-size: 28rpx;
color: #333;
}
.info-sub {
color: #999;
font-size: 24rpx;
}
.info-arrow {
font-size: 36rpx;
color: #999;
}
}
.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: 100%;
height: 88rpx;
background: #5CE5D5;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 500;
color: #333;
}
}
.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>