first commit

This commit is contained in:
2025-08-06 17:17:48 +08:00
commit dc5fba137c
453 changed files with 85995 additions and 0 deletions
+980
View File
@@ -0,0 +1,980 @@
<template>
<view class="goods-details">
<navbar title="商品详情" :background="{background: `rgba(256,256,256,${percent})`}" :titleColor="`rgba(0,0,0,${percent})`" :immersive="true"></navbar>
<!-- #ifdef H5 -->
<download-nav v-if="showDownload" :top="44"></download-nav>
<!-- #endif -->
<loading-view v-if="isFirstLoading"></loading-view>
<view class="contain" v-if="!isNull">
<bubble-tips top="180rpx"></bubble-tips>
<product-swiper :imgUrls="swiperList" :video="goodsDetail.video"></product-swiper>
<!-- 秒杀 -->
<view class="seckill row-between" v-if="goodsType == 1">
<view class="price row">
<view class="row white info">
<view style="align-items: baseline;" class="row ml20">
<view class="mr10">秒杀价</view>
<price-format :first-size="46" :second-size="32" :subscript-size="32"
:price="goodsDetail.min_price" :weight="500"></price-format>
<template v-if="goodsDetail.min_price != goodsDetail.max_price">
<text style="font-size: 46rpx;">-</text>
<price-format :first-size="46" :second-size="32" :subscript-size="32"
:show-subscript="false" :price="goodsDetail.max_price" :weight="500"></price-format>
</template>
<view class="ml10">
<price-format :subscript-size="30" :line-through="true" :first-size="30"
:second-size="30" :price="goodsDetail.market_price">
</price-format>
</view>
</view>
</view>
</view>
<view class="down column-center">
<view class="xxs primary mb10">距活动结束仅剩</view>
<u-count-down :timestamp="countTime" @end="getGoodsDetailFun" color="#fff" bg-color="#FF2C3C"
separator-color="#FF2C3C" font-size="24" height="36" separator-size="26"></u-count-down>
</view>
</view>
<!-- 拼团 -->
<view class="group" v-show="goodsType == 2">
<view class="row info" style="height: 100%">
<view class="row-between ml20 white" style="flex: 1;">
<view style="align-items: baseline;" class="row">
<view class="mr10">拼团价</view>
<price-format :subscript-size="32" :first-size="46" :second-size="32"
:price="team.team_min_price" :weight="500"></price-format>
<text class="xs"></text>
</view>
<view class="mr20 row group-num">
<view class="group-icon">
<image src="/static/images/icon_group.png" class="icon-sm"></image>
</view>
<view class="xxs ml10 mr10">{{ team.people_num }}人团</view>
</view>
</view>
<view class="down column-center">
<view class="xxs primary mb10">距活动结束仅剩</view>
<u-count-down :timestamp="countTime" color="#fff" bg-color="#FF2C3C" separator-color="#FF2C3C"
font-size="24" height="36" separator-size="26" @end="getGoodsDetailFun"></u-count-down>
</view>
</view>
</view>
<view class="goods-info bg-white">
<view class="info-header row" v-if="goodsType != 1">
<view class="price row flex1">
<view class="primary mr10">
<price-format :first-size="46" :second-size="32" :subscript-size="32"
:price="goodsDetail.min_price" :weight="500"></price-format>
<template v-if="goodsDetail.min_price != goodsDetail.max_price">
<text style="font-size: 46rpx;">-</text>
<price-format :first-size="46" :second-size="32" :subscript-size="32"
:show-subscript="false" :price="goodsDetail.max_price" :weight="500"></price-format>
</template>
</view>
<view class="line-through muted md">
<price-format :price="goodsDetail.market_price"></price-format>
</view>
</view>
<image class="icon-share" src="/static/images/icon_share.png" @tap="showShareBtn = true"></image>
</view>
<view class="row" v-if="!goodsType && (goodsDetail.member_price)">
<view class="vip-price row">
<view class="price-name xxs">会员价</view>
<view style="padding: 0 11rpx">
<price-format :price="goodsDetail.member_price " :first-size="26" :second-size="26"
:subscript-size="22" :weight="500" color="#7B3200">
</price-format>
<text class="xxs" style="color: #7B3200;"></text>
</view>
</view>
</view>
<view class="row">
<view class="name lg bold">{{ goodsDetail.name }}</view>
<image class="icon-share" src="/static/images/icon_share.png" @tap="showShareBtn = true"
v-if="goodsType == 1"></image>
</view>
<view class="row-between xs lighter" style="padding: 0 24rpx 20rpx">
<text v-if="goodsDetail.stock !== true">库存: {{ goodsDetail.stock }}</text>
<text>销量: {{ goodsDetail.sales_sum }}</text>
<text>浏览量: {{ goodsDetail.click_count }}</text>
</view>
</view>
<view class="group-play bg-white mt20" v-if="goodsType == 2">
<view class="title">拼团玩法</view>
<view class="steps row">
<view class="row step">
<view class="number xxs">1</view>
<view class="sm">开团/参团</view>
</view>
<view class="line"></view>
<view class="row step">
<view class="number xxs">2</view>
<view class="sm">团满即成新团</view>
</view>
<view class="line"></view>
<view class="row step">
<view class="number xxs">3</view>
<view class="sm">满员发货</view>
</view>
</view>
</view>
<view class="discount mt20 bg-white" v-if="couponList.length || goodsDetail.order_give_integral">
<view class="row" style="align-items: flex-start;">
<view class="text muted">优惠</view>
<view style="flex: 1">
<view :class="['row coupons', {mb30: goodsDetail.order_give_integral > 0}]"
v-if="couponList.length" @tap="showCouponFun">
<view class="flexnone">
<u-tag text="领券" size="mini" type="primary" mode="plain" />
</view>
<view class="con row ml20" style="flex: 1">
<view v-for="(item, index) in couponList" :key="index" class="coupons-item mr20">
<view v-if="index < 2" class="row xs">
<view class="line1">
{{ item.use_condition }}
</view>
</view>
</view>
</view>
<image class="icon-sm" src="/static/images/arrow_right.png"></image>
</view>
<view class="row integral" style="align-items: flex-start;"
v-if="goodsDetail.order_give_integral">
<view class="flexnone">
<u-tag text="积分" size="mini" type="primary" mode="plain" />
</view>
<view class="ml20">下单最多可获得{{goodsDetail.order_give_integral}}积分</view>
</view>
</view>
</view>
</view>
<swiper v-if="teamFound.length" class="mt20 bg-white" autoplay="true" style="height: 240rpx;"
vertical="true" circular="true" :interval="5000">
<swiper-item v-for="(sitem, index) in teamFound" :key="index">
<view class="group-list">
<view v-for="(item, index2) in sitem" :key="index2" class="group-item bg-white row-between">
<view class="row" style="max-width: 280rpx;">
<custom-image :src="item.avatar" width="80rpx" height="80rpx" radius="50%">
</custom-image>
<view class="ml20 line1 normal">{{ item.nickname }}</view>
</view>
<view class="row ml20" style="flex: none;">
<view class="column-center">
<text class="sm normal">
还差
<text class="primary">{{ item.need - item.join }}</text>
人成团
</text>
<view class="muted xs">
剩余
<u-count-down :timestamp="getTeamCountTime(item.found_end_time)"
separator-color="#999" color="#999" :separator-size="24" :font-size="24"
bg-color="transparent" @end="getGoodsDetailFun"></u-count-down>
</view>
</view>
<view class="group-btn br60 white row-center" @tap="showSpecFun(3, item.id)">去参团</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
<view v-if="!goodsType" class="spec row bg-white mt20" @tap="showSpecFun(0)">
<view class="text lighter">已选</view>
<view class="line1 mr20" style="flex: 1;">{{ checkedGoods.spec_value_str || '默认' }}</view>
<image class="icon-sm" src="/static/images/arrow_right.png"></image>
</view>
<navigator class="mt20" hover-class="none" url="/bundle/pages/server_explan/server_explan?type=2">
<view class="row bg-white" style="padding: 24rpx 24rpx;">
<view class="text lighter flex1">售后保障</view>
<image class="icon-sm" src="/static/images/arrow_right.png"></image>
</view>
</navigator>
<view class="evaluation bg-white mt20">
<navigator hover-class="none" :url="'/pages/all_comments/all_comments?id=' + goodsDetail.id"
class="title row-between">
<view>
<text class="balck md mr10">用户评价</text>
<text class="primary sm">好评率{{ comment.goods_rate || '0%' }}</text>
</view>
<view class="row">
<text class="lighter">查看全部</text>
<image class="icon-sm" src="/static/images/arrow_right.png"></image>
</view>
</navigator>
<view class="con" v-if="comment.goods_rate">
<view class="user-info row">
<image class="avatar mr20" :src="comment.avatar"></image>
<view class="user-name md mr10">{{ comment.nickname }}</view>
</view>
<view class="muted xs mt10">
<text class="mr20">{{ comment.create_time }}</text>
</view>
<view v-if="comment.comment" class="dec mt20">{{ comment.comment }}</view>
</view>
<view class="con row-center muted" v-else>暂无评价</view>
</view>
<view class="goods-like mt20 bg-white" v-if="goodsLike.length">
<goods-like :list="goodsLike"></goods-like>
</view>
<view class="details mt20 bg-white">
<view class="title lg">商品详情</view>
<view class="content">
<u-parse :html="goodsDetail.content" :lazy-load="true" :show-with-animation="true"></u-parse>
</view>
</view>
<view class="footer row bg-white fixed">
<navigator class="btn column-center" hover-class="none"
url="/bundle/pages/contact_offical/contact_offical">
<image class="icon-md" src="/static/images/icon_contact.png"></image>
<text class="xxs lighter">客服</text>
</navigator>
<button class="btn column-center" hover-class="none" @tap="collectGoodsFun">
<image class="icon-md"
:src="goodsDetail.is_collect == 1 ? '/static/images/icon_collection_s.png' : '/static/images/icon_collection.png'">
</image>
<text class="xxs lighter">收藏</text>
</button>
<navigator class="btn cart column-center" hover-class="none" open-type="switchTab"
url="/pages/shop_cart/shop_cart">
<image class="icon-md" src="/static/images/icon_cart.png"></image>
<text class="xxs lighter">购物车</text>
<u-badge v-if="cartNum" bgColor="#FF2C3C" :offset="[8, 10]" :count="cartNum"></u-badge>
</navigator>
<view v-if="btnText.yellow" class="add-cart br60 white mr10 md ml20" @tap="showSpecFun(1)">
{{ btnText.yellow }}
</view>
<view class="right-buy br60 white mr20 ml10 md" @tap="showSpecFun(2)">{{ btnText.red }}</view>
</view>
</view>
<view v-else>
<view class="details-null column-center">
<image class="img-null" src="/static/images/goods_null.png"></image>
<view class="xs muted">该商品已下架或不存在去逛逛别的吧~</view>
</view>
<recommend></recommend>
</view>
<spec-popup :show="showSpec" :goods="goodsDetail" :is-seckill="goodsType == 1" @close="showSpec = false"
:show-add="popupType == 1 || popupType == 0" :show-buy="popupType == 2 || popupType == 0"
:showConfirm="popupType == 3" @buynow="onBuy" @addcart="onAddCart" @change="onChangeGoods"
:group="Boolean(isGroup)" :red-btn-text="btnText.red" :yellow-btn-text="btnText.yellow"
@confirm="onConfirm"></spec-popup>
<share-popup v-model="showShareBtn"
:share-id="id"
pagePath="pages/goods_details/goods_details"
:type="1"
:config="{
avatar: userInfo.avatar,
nickname: userInfo.nickname,
image: goodsDetail.poster || goodsDetail.image,
price: goodsDetail.min_price,
marketPrice: goodsDetail.market_price,
name: goodsDetail.name
}">
</share-popup>
<!-- 领券 -->
<u-popup v-model="showCoupon" mode="bottom" border-radius="14">
<view>
<view class="row-between" style="padding: 30rpx">
<view class="title md bold">领券</view>
<view class="close" @tap="showCoupon = false">
<image class="icon-lg" src="/static/images/icon_close.png"></image>
</view>
</view>
<view class="content bg-body">
<scroll-view scroll-y="true" style="height: 700rpx">
<coupon-list :list="couponList" @reflash="getGoodsCouponFun" :btn-type="3"></coupon-list>
</scroll-view>
</view>
</view>
</u-popup>
<view class="share-money" :class="{ show: showCommission && enableCommission}">
<view class="row-end">
<view class="share-close row-center" @tap="showCommission=false">
<u-icon name="close" size="16" color="#fff"></u-icon>
</view>
</view>
<view class="share-con mt10" @tap="showShareBtn=true">
<view class="primary" style="font-size: 45rpx;">
{{distribution.earnings}}<text class="xs"></text>
</view>
<view class="lighter xxs">
好友下单最高可赚
</view>
</view>
</view>
<u-back-top :scroll-top="scrollTop" :top="1000" :customStyle="{ backgroundColor: '#FFF', color: '#000', boxShadow: '0px 3px 6px rgba(0, 0, 0, 0.1)'}"></u-back-top>
</view>
</template>
<script>
import {
getGoodsDetail,
addCart,
getPoster,
getCartNum
} from '@/api/store';
import {
collectGoods
} from '@/api/user';
import {
getGoodsCoupon,
teamCheck
} from '@/api/activity';
import {
mapActions,
mapGetters
} from 'vuex';
import {
arraySlice,
trottle
} from '@/utils/tools';
import {
toLogin
} from '@/utils/login';
import {
getUser,
inputInviteCode
} from '@/api/user';
import Cache from '@/utils/cache';
import {
strToParams
} from '@/utils/tools'
export default {
data() {
return {
scrollTop: 0,
percent: 0,
isFirstLoading: true,
isNull: false,
showSpec: false,
showCoupon: false,
showShareBtn: false,
showCommission: true,
popupType: '',
swiperList: [],
goodsDetail: {},
goodsLike: [],
goodsType: 0,
checkedGoods: {},
couponList: [],
comment: {},
countTime: 0,
tagStyle: {
img: 'width:100%;'
},
team: {},
teamFound: [],
isGroup: 0,
id: '',
showDownload: false,
distribution: {}
};
},
onLoad(options) {
this.onPageScroll = trottle(this.onPageScroll, 500, this)
if (options && options.scene) {
let scene = strToParams(decodeURIComponent(options.scene));
console.log(scene, decodeURIComponent(options.scene))
options.id = scene.id;
}
// #ifdef H5
if (options && options.isapp == 1) {
this.showDownload = true;
}
// #endif
if (!options.id) {
return this.$toast({
title: '缺少参数,无法查看商品'
}, {
tab: 3
});
} else {
this.id = options.id;
}
this.getGoodsCouponFun();
this.getCartNum();
},
onShow() {
this.getGoodsDetailFun();
},
onPageScroll(e) {
const top = uni.upx2px(100)
const {
scrollTop
} = e
this.percent = scrollTop / top > 1 ? 1 : scrollTop / top
this.scrollTop = scrollTop
},
methods: {
...mapActions(['getCartNum']),
async getGoodsDetailFun() {
const {
data,
code
} = await getGoodsDetail({
id: this.id
});
if (code == 1) {
let {
goods_image,
content,
comment,
like,
activity,
distribution
} = data;
let {
info,
team,
team_found
} = activity; //秒杀时间
let time = info ?
info.end_time - Date.now() / 1000 //拼团时间
:
team ?
team.end_time - Date.now() / 1000 :
0;
if (team_found) {
team_found = arraySlice(team_found, [], 2);
}
this.distribution = distribution || {}
this.goodsDetail = data;
this.swiperList = goods_image;
this.comment = comment;
this.goodsLike = like;
this.countTime = time;
this.goodsType = activity.type || 0;
this.team = team ? team : {};
this.teamFound = team_found ? team_found : [];
this.$nextTick(() => {
this.isFirstLoading = false;
});
// #ifdef H5
let options = {
shareTitle: data.name,
shareLink: location.href + '&invite_code=' + this.userInfo.distribution_code,
shareImage: data.image,
shareDesc: data.remark
};
this.wxShare(options);
// #endif
} else {
this.isNull = true
this.isFirstLoading = false;
}
},
async getGoodsCouponFun() {
const {
data,
code
} = await getGoodsCoupon({
id: this.id
});
if (code == 1) {
this.couponList = data;
}
},
async collectGoodsFun() {
if (!this.isLogin) return toLogin();
const {
goodsDetail: {
is_collect
}
} = this;
const {
data,
code
} = await collectGoods({
is_collect: is_collect == 0 ? 1 : 0,
goods_id: this.id
});
if (code == 1) {
if (is_collect == 0) {
this.$toast({
title: '收藏成功'
});
} else {
this.$toast({
title: '取消收藏'
});
}
this.getGoodsDetailFun();
}
},
showCouponFun() {
if (!this.isLogin) return toLogin();
this.showCoupon = true;
},
onChangeGoods(e) {
console.log(e);
this.checkedGoods = e.detail;
},
showSpecFun(type, id) {
if (!this.isLogin) return toLogin();
if (this.goodsType == 2 && [2, 3].includes(type)) {
this.isGroup = 1;
this.foundId = id;
} else {
this.isGroup = 0;
this.foundId = '';
}
this.popupType = type;
this.showSpec = true;
},
onBuy(e) {
let {
id,
goodsNum
} = e.detail;
const {
goodsType,
team
} = this;
let goods = [{
item_id: id,
num: goodsNum
}];
const params = {
goods,
};
this.showSpec = false;
goodsType == 2 ? (params.teamId = team.team_id) : '';
this.foundId ? (params.foundId = this.foundId) : '';
uni.navigateTo({
url: '/pages/confirm_order/confirm_order?data=' + encodeURIComponent((JSON.stringify(params)))
})
console.log(1111)
},
onConfirm(e) {
const {
team: {
team_id
}
} = this;
teamCheck({
team_id,
found_id: this.foundId
}).then(res => {
if (res.code == 1) {
this.onBuy(e);
}
});
},
async onAddCart(e) {
let {
id,
goodsNum
} = e.detail;
if (this.goodsType == 2) {
// 拼团单独购买
let goods = [{
item_id: id,
num: goodsNum
}];
uni.navigateTo({
url: '/pages/confirm_order/confirm_order?data=' + encodeURIComponent((JSON.stringify({
goods
})))
})
return
}
const {
code,
data,
msg
} = await addCart({
item_id: id,
goods_num: goodsNum
});
if (code == 1) {
this.getCartNum();
this.$toast({
title: msg,
icon: 'success'
});
this.showSpec = false;
}
}
},
async onShareAppMessage() {
const {
goodsDetail,
team,
userInfo
} = this;
return {
title: team.share_title || goodsDetail.name,
imageUrl: goodsDetail.image,
path: '/pages/goods_details/goods_details?id=' + this.id + "&invite_code=" + userInfo.distribution_code
};
},
computed: {
...mapGetters(['cartNum', 'userInfo']),
btnText() {
const {
goodsType
} = this;
switch (goodsType) {
case 1:
return {
red: '立即抢购',
yellow: ''
};
case 2:
return {
red: '立即开团',
yellow: '单独购买'
};
default:
return {
red: '立即购买',
yellow: '加入购物车'
};
}
},
getTeamCountTime() {
return time => time - Date.now() / 1000;
},
enableCommission() {
const {
goodsType,
distribution: {
earnings,
is_show
}
} = this
return goodsType == 0 && earnings > 0 && is_show == 1
}
}
};
</script>
<style lang="scss" scoped>
.goods-details {
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
.seckill {
height: 100rpx;
background: #ffd4d8;
.price {
width: 504rpx;
height: 100%;
background: url(../../static/images/bg_seckill.png) no-repeat;
background-size: 100%;
}
.down {
flex: 1;
}
}
.group {
height: 100rpx;
width: 100%;
background-image: url(../../static/images/pintuan_bg.png);
background-size: 100%;
.group-num {
border: 1px solid #ffffff;
border-radius: 4rpx;
.group-icon {
background: #fff;
padding: 3rpx 7rpx;
}
}
.down {
height: 100%;
background-color: #fff5e1;
padding: 0 20rpx;
}
}
.goods-info {
position: relative;
.info-header {
padding: 20rpx 0 0rpx 24rpx;
.price {
align-items: baseline;
}
}
.vip-price {
margin: 0 24rpx;
background-color: #FFE9BA;
color: #FFD4B7;
line-height: 36rpx;
border-radius: 6rpx;
overflow: hidden;
.price-name {
background-color: #101010;
padding: 3rpx 12rpx;
position: relative;
overflow: hidden;
&::after {
content: '';
display: block;
width: 20rpx;
height: 20rpx;
position: absolute;
right: -15rpx;
background-color: #FFE9BA;
border-radius: 50%;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
}
}
}
.name {
padding: 20rpx 24rpx;
flex: 1;
}
.icon-share {
width: 134rpx;
height: 60rpx;
}
}
.discount {
padding: 24rpx;
.text {
width: 100rpx;
flex: none;
}
.con {
width: 400rpx;
}
.coupons-item {
overflow: hidden;
&>view {
position: relative;
height: 40rpx;
line-height: 40rpx;
padding: 0 18rpx;
border-radius: 6rpx;
box-sizing: border-box;
background-color: $-color-primary;
color: #fff;
white-space: nowrap;
overflow: hidden;
&::after,
&::before {
content: '';
display: block;
width: 20rpx;
height: 20rpx;
position: absolute;
left: -14rpx;
background-color: #fff;
border-radius: 50%;
border: 1px solid currentColor;
box-sizing: border-box;
}
&::after {
right: -14rpx;
left: auto;
}
}
}
}
.details-null {
padding-top: 140rpx;
margin-bottom: 100rpx;
}
.spec {
padding: 24rpx;
.text {
width: 100rpx;
}
}
.evaluation {
.title {
height: 100rpx;
border-bottom: $-solid-border;
padding: 0 24rpx;
}
.con {
padding: 30rpx 24rpx;
}
.user-info .avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
}
}
.details {
.title {
line-height: 88rpx;
text-align: center;
}
&>.content {
padding: 0 20rpx 20rpx;
overflow: hidden;
::v-deep image {
vertical-align: middle;
}
// #ifdef H5
::v-deep img {
vertical-align: middle;
}
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
::v-deep ._img {
display: block;
}
// #endif
}
}
.footer {
height: 100rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
box-sizing: content-box;
padding-bottom: env(safe-area-inset-bottom);
.btn {
width: 100rpx;
height: 100rpx;
position: relative;
line-height: 1.3;
}
.cart-num {
position: absolute;
left: 60rpx;
top: 6rpx;
}
.add-cart,
.right-buy {
flex: 1;
text-align: center;
padding: 16rpx 0;
}
.add-cart {
background-color: #ffa630;
}
.right-buy {
background-color: $-color-primary;
}
}
.group-play {
.title {
padding: 20rpx 28rpx;
border-bottom: $-solid-border;
}
.steps {
padding: 20rpx 28rpx;
.step {
flex: none;
}
.line {
flex: 1;
border: 1px dashed #999999;
margin: 0 20rpx;
}
.number {
border: 1rpx solid #707070;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
line-height: 28rpx;
text-align: center;
margin-right: 6rpx;
}
}
}
.group-list {
.group-item {
padding: 20rpx 24rpx;
&:not(:last-of-type) {
border-bottom: $-solid-border;
}
.group-btn {
background: linear-gradient(90deg, #f95f2f 0%, #ff2c3c 100%);
height: 58rpx;
padding-left: 28rpx;
padding-right: 28rpx;
margin-left: 30rpx;
box-shadow: 0px 6rpx 12rpx rgba(249, 47, 138, 0.4);
}
}
}
.share-money {
position: fixed;
left: 20rpx;
bottom: calc(130rpx + env(safe-area-inset-bottom));
transform: scale(0);
transition: all .3s;
&.show {
transform: scale(1);
}
.share-close {
width: 34rpx;
height: 34rpx;
background: #a7a7a7;
border-radius: 50%;
}
.share-con {
background: url('../../static/images/bg_packet_img.png');
width: 241rpx;
height: 208rpx;
background-size: 100%;
padding-top: 20rpx;
text-align: center;
}
}
}
</style>