glhcp/pc/pages/goods_details/_id.vue

864 lines
29 KiB
Vue

<template>
<div>
<div class="goods-details" v-if="goodsDetails.id">
<div class="goods-info flex col-stretch">
<div class="goods-swiper m-r-16 bg-white flex-col" v-if="goodsImage != ''">
<el-image class="current-img" :preview-src-list="goodsImage.map((item) => item.uri)"
:src="goodsImage[swiperIndex].uri"></el-image>
<client-only>
<swiper class="swiper" ref="mySwiper" :options="swiperOptions">
<swiper-slide :class="{
'swiper-item': true,
active: index === swiperIndex,
}" v-for="(item, index) in goodsImage" :key="index">
<div style="width: 100%; height: 100%" @mouseover="swiperIndex = index">
<el-image style="width: 100%; height: 100%" :src="item.uri"></el-image>
</div>
</swiper-slide>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</client-only>
</div>
<!-- 右侧商品信息 -->
<div class="info-wrap bg-white flex-1">
<div class="name weight-500 m-b-16">
{{ goodsDetails.name }}
</div>
<!-- -->
<div class="seckill flex white" v-if="activity.type == 1">
<div class="xxl">限时秒杀</div>
<div class="flex">
<div class="white m-r-16">距离结束还有</div>
<count-down :time="countTime" :is-slot="true" @change="onChangeDate">
<div class="flex row-center count-down xxl">
<div class="item white">{{ timeData.hours }}</div>
<div class="white" style="margin: 0 4px">:</div>
<div class="item white">{{ timeData.minutes }}</div>
<div class="white" style="margin: 0 4px">:</div>
<div class="item white">{{ timeData.seconds }}</div>
</div>
</count-down>
</div>
</div>
<!-- -->
<!-- -->
<div class="price-wrap lighter">
<div class="flex row-between">
<div class="price">
<div class="flex" v-if="Number(checkedGoods.market_price || goodsDetails.market_price)">
{{ activity.type == 1 ? '日常价' : '原价' }}
<span class="m-l-20">
<price-formate :price="checkedGoods.market_price || goodsDetails.market_price" :weight="400" />
</span>
</div>
<div class="flex m-t-10">
{{ activity.type == 1 ? '秒杀价' : '价格' }}
<div class="primary m-l-20">
<price-formate :price="checkedGoods.price || goodsDetails.min_price"
:subscript-size="16" :first-size="26" :second-size="16" />
</div>
<div class="member-price m-l-10 flex xs"
v-if="activity.type != 1 && (checkedGoods.member_price || goodsDetails.member_price)">
会员价
<price-formate :price="checkedGoods.member_price || goodsDetails.member_price"
:weight="400" />
</div>
</div>
</div>
<div class="rate flex-col row-right">
<div class="primary" style="font-size: 20px">{{ comment.percent }}</div>
<div class="lighter">好评率</div>
</div>
</div>
</div>
<!-- -->
<div class="sales-click flex">
<div class="flex-1">销量:{{ goodsDetails.sales_sum }}件</div>
<div class="flex-1">浏览量:{{ goodsDetails.clicks }}次</div>
</div>
<div class="spec-wrap">
<div class="spec flex m-b-16" v-for="(item, index) in goodsSpec" :key="index">
<div class="lighter spec-name">{{ item.name }}</div>
<div class="spec-list flex flex-wrap">
<div :class="[
'spec-item lighter',
{ active: specitem.checked },
]" v-for="(specitem, sindex) in item.spec_value" :key="sindex"
@click="onChoseSpecItem(item.id, specitem.id)">
{{ specitem.value }}
</div>
</div>
</div>
</div>
<div class="goods-num flex">
<div class="num lighter">数量</div>
<number-box v-model="goodsNum" :min="1" :max="checkedGoods.stock" />
<div class="m-l-10">
库存: {{ checkedGoods.stock }}
</div>
</div>
<div class="goods-btns flex lg">
<el-button type="primary" class="btn white" @click="onBuyNow">
立即购买
</el-button>
<el-button type="primary" plain class="btn addcart" @click="onAddCart"
v-if="goodsDetails.type == 0 && activity.type != 1">
加入购物车
</el-button>
<div class="btn collection flex row-center" @click="onCollectionGoods">
<img class="start-icon m-r-8" :src="
goodsDetails.is_collect
? require('~/static/images/icon_star_s.png')
: require('~/static/images/icon_star.png')
" />
<span>{{ goodsDetails.is_collect ? '取消收藏' : '收藏商品' }}</span>
</div>
</div>
</div>
<div class="shop m-l-16 bg-white">
<div class="shop-logo flex-col col-center">
<el-image class="logo-img" :src="shop.logo"></el-image>
<nuxt-link class="m-t-10" :to="`/shop_street_detail?id=${shop.id}`">
<el-tag size="mini" v-if="shop.type == 1">自营</el-tag>
<span class="weight-500">{{ shop.name }}</span>
</nuxt-link>
<div class="xs muted m-t-10 line-5">
{{ shop.intro }}
</div>
</div>
<div class="flex m-t-20">
<div class="flex-1 text-center">
<div class="xxl m-b-10">{{ shop.goods_on_sale }}</div>
<div>全部商品</div>
</div>
<div class="flex-1 text-center">
<div class="xxl m-b-10">{{ shop.follow_num }}</div>
<div>关注人数</div>
</div>
</div>
<el-divider></el-divider>
<div class="flex xs m-b-16">
<div class="m-r-12">店铺星级</div>
<div class="m-t-5">
<el-rate v-model="shop.star" disabled> </el-rate>
</div>
</div>
<div class="flex xs m-b-16">
<div class="m-r-12">店铺评分</div>
<div class="">{{ shop.score }}分</div>
</div>
<div>
<el-button type="primary" size="mini" @click="
$router.push(`/shop_street_detail?id=${shop.id}`)
">进入店铺</el-button>
<el-button size="mini" @click="changeShopFollow">{{
shop.shop_follow_status == 1 ? '已关注' : '关注店铺'
}}</el-button>
</div>
<el-popover placement="bottom" width="200" trigger="hover">
<div>
<el-image style="width: 100%" :src="shop.customer_image"></el-image>
</div>
<div slot="reference" class="xs lighter text-center m-t-30">
<i class="el-icon-chat-dot-round nr"></i>
<span>联系客服</span>
</div>
</el-popover>
</div>
</div>
<div class="details-wrap flex m-t-16">
<div class="details bg-white flex-1">
<el-tabs v-model="active">
<el-tab-pane label="商品详情">
<div class="details-tips" v-if="goodsDetails.custom_params.length">
<h2>产品参数</h2>
<div class="boxs">
<div class="tit" v-for="(items,index) in goodsDetails.custom_params" :key="index">
<span class="st">{{items.key}}:</span>{{items.value}}
</div>
</div>
</div>
<div class="rich-text" v-html="goodsDetails.content"></div>
</el-tab-pane>
<el-tab-pane label="商品评价">
<div class="evaluation">
<div class="evaluation-hd flex">
<div class="rate flex">
<div class="lighter m-r-8">好评率</div>
<div class="primary" style="font-size: 30px">
{{ goodsDetails.comment.percent }}
</div>
</div>
<div class="score flex">
<span class="m-r-8 lighter">评分</span>
<el-rate :value="goodsDetails.comment.goods_comment" disabled allow-half></el-rate>
</div>
</div>
<div class="evaluation-tab flex">
<div :class="[
'item',
{ active: commentActive == item.id },
]" v-for="(item, index) in comment.comment" :key="index"
@click="commentActive = item.id">
{{ item.name }}({{ item.count }})
</div>
</div>
</div>
<div>
<template v-for="(item, index) in comment.comment">
<comment-list v-if="commentActive == item.id" :goods-id="id" :type="item.id"
:key="index"></comment-list>
</template>
</div>
</el-tab-pane>
</el-tabs>
</div>
<div class="goods-like m-l-16" v-if="shop.goods_list.length">
<div class="title bg-white flex p-l-15">店铺推荐</div>
<div class="goods-list">
<template v-for="(item, index) in shop.goods_list">
<nuxt-link :to="`/goods_details/${item.id}`" class="item bg-white m-b-16" v-if="index < 5"
:key="index">
<el-image class="goods-img" :src="item.image"></el-image>
<div class="goods-name line-2">
{{ item.name }}
</div>
<div class="price flex m-t-8">
<div class="primary m-r-8">
<price-formate :price="item.min_price" :first-size="16" />
</div>
<div class="muted sm line-through">
<price-formate :price="item.market_price" />
</div>
</div>
</nuxt-link>
</template>
</div>
</div>
</div>
</div>
<!-- 推荐 -->
<div class="activity-area m-t-16">
<div class="title flex row-between">
<div class="font-size-20">推荐商品</div>
</div>
<div class="active-goods flex">
<div class="goods-list flex">
<nuxt-link
class="goods-item"
:to="`/goods_details/${gitem.id}`"
v-for="(gitem, gindex) in dataRecommend"
:key="gindex"
>
<div class="goods-img">
<el-image
lazy
:src="gitem.image"
fit="cover"
alt=""
></el-image>
</div>
<div class="name line-2 m-t-10">{{ gitem.name }}</div>
<div class="price flex col-baseline m-t-10">
<div class="primary m-r-8">
<price-formate
:price="gitem.min_price"
:first-size="16"
/>
</div>
<div class="muted sm line-through ">
<price-formate
:price="gitem.market_price"
/>
</div>
</div>
<div class="muted xs m-t-10">
{{ gitem.sales_total }}
</div>
</nuxt-link>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
import { Message } from 'element-ui'
export default {
head() {
return {
title: this.$store.getters.headTitle,
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: this.$store.getters.favicon,
},
],
}
},
async asyncData({ params, $get, app }) {
const { data: dataRecommend } = await $get('goods/getRecommendGoods',{params: {goods_id: params.id}})
const { data, code, msg } = await $get('goods/getGoodsDetail', {
params: { goods_id: params.id },
})
if (code == 0) {
setTimeout(() => app.router.back(), 1500)
}
return {
dataRecommend: dataRecommend,
goodsDetails: data,
goodsImage: data.goods_image,
activity: data.activity,
shop: data.shop,
}
},
data() {
return {
goodsDetails: {},
goodsImage: [],
activity: {},
shop: {
goods_list: [],
},
swiperOptions: {
pagination: {
el: '.swiper-pagination',
clickable: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
preventClicks: true,
slidesPerView: 'auto',
},
active: '0',
commentActive: 0,
swiperIndex: 0,
checkedGoods: {},
comment: {},
goodsNum: 1,
goodsSpec: [],
id: '',
timeData: {},
dataRecommend: [], //推荐
}
},
created() {
this.id = this.$route.params.id
this.getComment(this.id)
},
methods: {
...mapActions(['getPublicData']),
onClickSlide(e) {
this.swiperIndex = e
},
onChoseSpecItem(id, specid) {
const { goodsSpec } = this
goodsSpec.forEach((item) => {
if (item.spec_value && item.id == id) {
item.spec_value.forEach((specitem) => {
specitem.checked = 0
if (specitem.id == specid) {
specitem.checked = 1
}
})
}
})
this.goodsSpec = [...goodsSpec]
},
async onAddCart() {
const {
goodsNum,
checkedGoods: { id },
} = this
const { code, data, msg } = await this.$post('cart/add', {
item_id: id,
goods_num: goodsNum,
})
if (code == 1) {
this.getPublicData()
this.$message({
message: msg,
type: 'success',
})
}
},
async changeShopFollow() {
const { code, msg } = await this.$post('shop_follow/changeStatus', {
shop_id: this.shop.id,
})
if (code == 1) {
this.$message({
message: msg,
type: 'success',
})
this.getGoodsDetail()
}
},
onBuyNow() {
const {
goodsNum,
checkedGoods: { id },
} = this
const goods = [
{
item_id: id,
num: goodsNum,
goods_id: this.id,
shop_id: this.shop.id,
},
]
this.$router.push({
path: '/confirm_order',
query: {
data: encodeURIComponent(
JSON.stringify({
goods,
type: 'buy',
})
),
},
})
},
async getGoodsDetail() {
const { data, code } = await this.$get('goods/getGoodsDetail', {
params: { goods_id: this.id },
})
if (code == 1) {
this.goodsDetails = data
this.shop = data.shop
}
},
async onCollectionGoods() {
const { data, code, msg } = await this.$post(
'goods_collect/changeStatus',
{
goods_id: this.id,
}
)
if (code == 1) {
this.$message({
message: msg,
type: 'success',
})
this.getGoodsDetail()
}
},
async getComment() {
const { data, code } = await this.$get('/goods_comment/category', {
params: { goods_id: this.id },
})
if (code == 1) {
this.comment = data
this.commentActive = data.comment[0].id
}
},
onChangeDate(e) {
let timeData = {}
for (let prop in e) {
if (prop !== 'milliseconds')
timeData[prop] = ('0' + e[prop]).slice(-2)
}
this.timeData = timeData
},
},
watch: {
goodsSpec: {
immediate: true,
handler(value) {
const { goods_item } = this.goodsDetails
let keyArr = []
value.forEach((item) => {
if (item.spec_value) {
item.spec_value.forEach((specitem) => {
if (specitem.checked) {
keyArr.push(specitem.id)
}
})
}
})
if (!keyArr.length) return
let key = keyArr.join(',')
let index = goods_item.findIndex((item) => {
return item.spec_value_ids == key
})
if (index == -1) {
index = 0
}
this.checkedGoods = goods_item[index]
console.log(this.checkedGoods)
},
},
goodsDetails: {
immediate: true,
handler(value) {
if (!value.goods_spec) return
value.goods_spec.forEach((item) => {
item.spec_value.forEach((specitem, specindex) => {
if (specindex == 0) {
specitem.checked = 1
} else {
specitem.checked = 0
}
})
})
this.goodsSpec = [...value.goods_spec]
},
},
},
computed: {
countTime() {
const end_time = this.activity.end_time
return end_time ? end_time - Date.now() / 1000 : 0
},
},
}
</script>
<style lang="scss" scoped>
.goods-details {
padding: 16px 0 44px;
.goods-info {
.goods-swiper {
width: 400px;
border-radius: 4px;
.swiper {
margin: 10px 0;
padding: 0 25px;
--swiper-navigation-size: 15px;
--swiper-navigation-color: #888;
.swiper-button-next,
.swiper-button-prev {
top: 0;
width: 25px;
height: 100%;
margin-top: 0;
background-size: 12px 22px;
}
.swiper-button-prev {
left: 0;
}
.swiper-button-next {
right: 0;
}
.swiper-item {
cursor: pointer;
height: 66px;
width: 66px;
border: 2px solid transparent;
&~.swiper-item {
margin-left: 10px;
}
&.active {
border-color: $--color-primary;
}
}
}
.current-img {
width: 100%;
height: 400px;
}
}
.info-wrap {
min-height: 486px;
min-width: 504px;
border-radius: 4px;
padding: 20px;
.name {
font-size: 20px;
}
.seckill {
background-color: $--color-primary;
padding: 6px 10px;
.count-down {
.item {
width: 30px;
height: 30px;
background: rgba(0, 0, 0, 0.3);
text-align: center;
line-height: 30px;
border-radius: 4px;
}
}
}
.price-wrap {
background: #f6f6f6;
background-size: 100%;
padding: 16px 16px;
.member-price {
background-color: #482406;
color: #fdebd5;
border-radius: 100px 100px 100px 0px;
padding: 0 6px;
}
}
.sales-click {
border-bottom: 1px dashed $--border-color-base;
line-height: 28px;
text-align: center;
padding: 6px;
&>div:first-of-type {
border-right: 1px solid $--border-color-base;
}
}
.spec-wrap {
margin-top: 20px;
.spec {
align-items: flex-start;
.spec-name {
margin-right: 20px;
margin-top: 6px;
flex: none;
}
.spec-item {
padding: 0 20px;
line-height: 32px;
border: $--border-base;
border-radius: 2px;
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
&.active {
color: $--color-primary;
background-color: #ffeeef;
border-color: currentColor;
}
}
}
}
.goods-num {
margin-bottom: 30px;
.num {
margin-right: 20px;
}
}
.goods-btns {
.btn {
margin-right: 14px;
text-align: center;
width: 120px;
font-size: 16px;
&.collection {
width: 146px;
line-height: 42px;
border: 1px solid rgba(229, 229, 229, 0.89804);
background-color: #fff;
border-radius: 4px;
cursor: pointer;
color: #666;
&:hover {
color: $--color-primary;
}
.start-icon {
width: 18.5px;
height: 18px;
}
}
}
}
}
.shop {
width: 210px;
padding: 16px;
.logo-img {
width: 62px;
height: 62px;
border-radius: 50%;
overflow: hidden;
}
.el-rate__icon {
font-size: 16px;
}
}
}
.details-wrap {
align-items: stretch;
.details {
padding: 10px 0;
overflow: hidden;
.rich-text {
padding: 0 10px;
width: 100%;
overflow: hidden;
img {
width: 100%;
display: block;
}
p {
margin: 0;
}
}
.evaluation {
.evaluation-hd {
height: 80px;
margin: 0 10px;
.rate {
height: 60px;
width: 220px;
border-right: 1px solid #e5e5e5;
padding-left: 10px;
margin-right: 40px;
}
}
.evaluation-tab {
margin: 16px 20px;
.item {
border-radius: 2px;
cursor: pointer;
height: 32px;
padding: 6px 20px;
color: #666;
background-color: #f2f2f2;
margin-right: 10px;
&.active {
color: #fff;
background-color: $--color-primary;
}
}
}
}
}
}
.goods-like {
width: 210px;
.title {
border-bottom: $--border-base;
height: 45px;
}
.goods-list {
.item {
padding: 10px;
display: block;
.goods-img {
width: 190px;
height: 190px;
margin-bottom: 10px;
}
}
}
}
}
.details-tips {
padding:20px;
h2 {
margin: 0 0 20px 0;
}
.boxs {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
column-gap: 10px;
row-gap: 10px;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: keep-all; /* 长单词换行 */
word-break: break-all; /* 单词内换行 */
.tit {
font-size: 14px;
.st {
font-size: 16px;
font-weight: bold;
margin-right: 5px;
}
}
}
}
.activity-area {
padding: 16px;
border-radius: 6px;
background-color: #fff;
.title {
font-size: 36px;
font-weight: bold;
margin-top: 10px;
margin-left: 10px;
}
.goods-list {
flex-wrap: wrap;
.goods-item {
width: 14.6%;
margin: 20px 1% 0 1%;
.goods-img {
width: 100%;
height: 0;
padding-top: 100%;
position: relative;
.el-image {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
}
.name {
line-height: 20px;
height: 40px;
}
}
}
}
</style>