课程名称: CRMEB uniapp电商项目二次开发实战
课程章节: 6-1–6-3 商品详情页规格选择组件开发一
课程讲师: CRMEB
课程内容:
1、首先创建商品详情页面goods_detail.vue
<template>
<view class="goods-detail" v-if="goodsDetail.storeInfo.id">
<view class="goods-detail-container">
<Banner :list="goodsDetail.storeInfo.slider_image"></Banner>
<BaseInfo :info="baseInfo"></BaseInfo>
<SpecsSelect :info="specsInfo" ref="specsSelect"></SpecsSelect>
<Description :info="description"></Description>
<GoodsAction
:info="goodsActionInfoData"
@collect="onCollection"
@cancel-collect="onCancelCollection"
@cart="onGoCart"
@add-cart="onAddCart">
</GoodsAction>
</view>
</view>
</template>
<script>
import authorizationMixin from '@/mixins/authorization'
import {
productDetail as productDetailApi,
collectProduct as collectProductApi,
cancelCollectProduct as cancelCollectProductApi
} from '@/api/goods'
import {
addCart as addCartApi,
getCartNum as getCartNumApi
} from '@/api/cart'
import Banner from './components/Banner'
import BaseInfo from './components/BaseInfo'
import SpecsSelect from './components/SpecsSelect'
import Description from './components/Description'
import GoodsAction from './components/GoodsAction'
export default {
mixins: [authorizationMixin],
components: {
Banner,
BaseInfo,
SpecsSelect,
Description,
GoodsAction
},
data() {
return {
id: 0,
goodsDetail: {
storeInfo: {}
},
cartNum: 0
}
},
computed: {
baseInfo () {
return {
price: this.goodsDetail.storeInfo.price,
vip_price: this.goodsDetail.storeInfo.vip_price,
store_name: this.goodsDetail.storeInfo.store_name,
ot_price: this.goodsDetail.storeInfo.ot_price,
stock: this.goodsDetail.storeInfo.stock,
fsales: this.goodsDetail.storeInfo.fsales,
unit_name: this.goodsDetail.storeInfo.unit_name
}
},
specsInfo () {
return {
productValue: this.goodsDetail.productValue,
productAttr: this.goodsDetail.productAttr,
store_name: this.goodsDetail.storeInfo.store_name
}
},
description () {
return this.goodsDetail.storeInfo.description ? this.goodsDetail.storeInfo.description.replace(/<img/g, '<img ') : ''
},
goodsActionInfoData () {
return {
id: this.id,
userCollect: this.goodsDetail.storeInfo.userCollect,
cartNum: this.cartNum
}
},
},
onLoad (options) {
this.id = options.gid ? options.gid : 0
this.getProductDetail()
if (this.isLogined()) {
this.getCartNum()
}
},
methods: {
onCollection () {
const job = {
name: '收藏商品',
// 自定义函数
funcs: [
],
// 页面方法
methods: [
],
// data数据相关字段对应的数据值
dataParams: {
}
}
if (!this.isLogined()) {
job.funcs.push({
body: (pagePath) => {
uni.navigateTo({
url: pagePath
})
},
args: [getCurrentPages().pop().$page.fullPath],
delay: 0
})
}
job.methods.push({
name: this.collectProduct,
delay: this.isLogined() ? 0 : 1000
})
this.needLoginCheckClickHandler(job)
},
onCancelCollection () {
this.cancelCollectProduct()
},
onGoCart () {
const job = {
name: '去购物车列表',
// 自定义函数
funcs: [
],
// 页面方法
methods: [
],
// data数据相关字段对应的数据值
dataParams: {
}
}
job.funcs.push({
body: (pagePath) => {
uni.switchTab({
url: pagePath
})
},
args: ['/pages/order_addcart/order_addcart'],
delay: 0
})
this.needLoginCheckClickHandler(job)
},
onAddCart () {
const job = {
name: '加入购物车',
// 自定义函数
funcs: [
],
// 页面方法
methods: [
],
// data数据相关字段对应的数据值
dataParams: {
}
}
if (!this.isLogined()) {
job.funcs.push({
body: (pagePath) => {
uni.navigateTo({
url: pagePath
})
},
args: [getCurrentPages().pop().$page.fullPath],
delay: 0
})
} else {
job.methods.push({
name: this.addCart,
delay: 0
})
}
this.needLoginCheckClickHandler(job)
},
async collectProduct () {
const params = {
id: this.id
}
const { status, msg } = await collectProductApi(params)
if (status === this.API_STATUS_CODE.SUCCESS) {
this.goodsDetail.storeInfo.userCollect = true
uni.showToast({
icon: 'none',
title: '收藏成功',
duration: 3000
})
} else {
uni.showToast({
icon: 'none',
title: msg,
duration: 3000
})
}
},
async cancelCollectProduct () {
const params = {
id: this.id
}
const { status, msg } = await cancelCollectProductApi(params)
if (status === this.API_STATUS_CODE.SUCCESS) {
this.goodsDetail.storeInfo.userCollect = false
uni.showToast({
icon: 'none',
title: '取消收藏成功',
duration: 3000
})
} else {
uni.showToast({
icon: 'none',
title: msg,
duration: 3000
})
}
},
async addCart () {
const specsSelectRef = this.$refs.specsSelect
if (false === specsSelectRef.popupIsShow) {
specsSelectRef.popupIsShow = true
return false
}
const params = {
cartNum: specsSelectRef.buyNum,
new: 0,
productId: this.id,
uniqueId: specsSelectRef.curSelectedSpecValue.unique
}
const { status, msg } = await addCartApi(params)
if (status === this.API_STATUS_CODE.SUCCESS) {
this.getCartNum()
uni.showToast({
icon: 'none',
title: '添加购物车成功',
duration: 3000
})
setTimeout(() => {
specsSelectRef.popupIsShow = false
}, 3000)
} else {
uni.showToast({
icon: 'none',
title: msg,
duration: 3000
})
}
},
async getCartNum () {
const { status, data, msg } = await getCartNumApi()
if (status === this.API_STATUS_CODE.SUCCESS) {
this.cartNum = data.count
} else {
uni.showToast({
icon: 'none',
title: msg,
duration: 3000
})
}
},
async getProductDetail () {
const { status, data, msg } = await productDetailApi(this.id)
if (status === this.API_STATUS_CODE.SUCCESS) {
this.goodsDetail = data
} else {
uni.showToast({
icon: 'none',
title: msg,
duration: 3000
})
}
}
}
}
</script>
<style lang="scss" scoped>
.goods-detail {
min-height: 100vh;
background-color: #f5f5f5;
}
</style>
2、新建轮播图组件Banner.vue
<template>
<view class="banner">
<view class="banner-container">
<swiper
class="swiper"
:indicator-dots="indicatorDots"
:autoplay="autoplay"
:interval="interval"
:duration="duration"
indicator-active-color="#ffffff">
<swiper-item
v-for="(src, idx) in list"
:key="idx">
<image :src="src" mode="aspectFit"></image>
</swiper-item>
</swiper>
</view>
</view>
</template>
<script>
export default {
props: {
list: {
type: Array,
default: () => []
}
},
data () {
return {
indicatorDots: true,
autoplay: true,
interval: 2000,
duration: 500
}
}
}
</script>
<style lang="scss" scoped>
.swiper {
height: 750rpx;
image {
width: 100%;
height: 750rpx;
}
}
</style>
3、新建规格选择组件BaseInfo.vue
<template>
<view class="base-info" v-if="info.store_name">
<view class="base-info-container">
<view class="price">
<view class="market-price">
¥
<text>{{ info.price }}</text>
起
</view>
<view class="vip-price">
¥{{ info.vip_price }}
<image :src="vipIcon"></image>
</view>
<view class="share iconfont icon-fenxiang"></view>
</view>
<view class="name">{{ info.store_name }}</view>
<view class="stock">
<text>原价:¥{{ info.ot_price }}</text>
<text>库存:{{ info.stock }} {{ info.unit_name }}</text>
<text>销量:{{ info.fsales }} {{ info.unit_name }}</text>
</view>
</view>
</view>
</template>
<script>
import vipIcon from '@/static/images/jvip.png'
export default {
props: {
info: {
type: Object,
default: () => {}
}
},
data () {
return {
vipIcon
}
}
}
</script>
<style lang="scss" scoped>
.base-info {
background-color: #fff;
&-container {
.price {
position: relative;
display: flex;
align-items: flex-end;
padding-top: 24rpx;
font-size: 28rpx;
font-weight: 700;
margin: 0 30rpx;
.market-price {
color: #fc4141!important;
text {
font-size: 48rpx;
}
}
.vip-price {
color: #282828;
margin-left: 12rpx;
image {
width: 46rpx;
height: 22rpx;
}
}
.share {
position: absolute;
right: 30rpx;
bottom: 10rpx;
color: #515151;
font-size: 40rpx;
}
}
.name {
font-size: 32rpx;
font-weight: 700;
margin: 22rpx 30rpx 0 30rpx;
word-break: break-all;
}
.stock {
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #82848f;
padding-bottom: 20rpx;
margin: 22rpx 30rpx 0;
}
}
}
</style>
课程收获:
这个节课主要学习到了image的图片裁剪、缩放的模式mode其中包括的参数有aspectFit( 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。)scaleToFill(不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素)aspectFill(保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。)widthFix(宽度不变,高度自动变化,保持原图宽高比不变)heightFix(高度不变,宽度自动变化,保持原图宽高比不变 App 和 H5 平台 HBuilderX 2.9.3+ 支持、微信小程序需要基础库 2.10.3)再有学习到了主页跳转到详情页面通过onLoad中获取传递参数