课程名称: CRMEB uniapp电商项目二次开发实战
课程章节: 6-7 商品详情页规格选择功能实现一
课程讲师: CRMEB
课程内容:
1、首先创建SpecsSelect.vue 构建选择页面
<template>
<view class="specs-select">
<view class="specs-select-container">
<view class="row" @click="onPopupShow">
<view class="lable">已选择:</view>
<view class="content">{{ selectedSpecKey }}</view>
<view class="iconfont icon-jiantou"></view>
</view>
<VanPopup
:show="popupIsShow"
:round="true"
position="bottom"
z-index="0"
class="popup">
<view class="header">
<image class="cover" mode="aspectFit" :src="curSelectedSpecValue.image"></image>
<view class="base-info">
<view class="name">{{ info.store_name }}</view>
<view class="stock">
<view class="price">
<text>¥</text>
{{ curSelectedSpecValue.price }}
</view>
库存:{{ curSelectedSpecValue.stock }}
</view>
</view>
<view class="iconfont icon-guanbi" @click="onPopupClose"></view>
</view>
<view class="body">
<scroll-view class="specs-scroll-view" scroll-y="true">
<view class="specs-list">
<view
v-for="(specs, idx) in infoData.productAttr"
:key="idx"
class="specs-item">
<view class="specs-item-title">{{ specs.attr_name }}</view>
<view class="specs-item-values">
<view
v-for="(val, idx2) in specs.attr_value"
:key="idx2"
:class="{ on: val.check }"
@click="onSelectSpec(idx, idx2, val)">
{{ val.attr }}
</view>
</view>
</view>
</view>
<view class="stepper">
<view class="stepper-lable">数量</view>
<view class="stepper-field">
<view
class="iconfont icon-shangpinshuliang-jian"
:class="{ disabled: disableMinus }"
@click="onMinus">
</view>
<input v-model="buyNum" @input="onInputBuyNum">
<view
class="iconfont icon-shangpinshuliang-jia"
:class="{ disabled: disablePlus }"
@click="onPlus">
</view>
</view>
</view>
</scroll-view>
</view>
</VanPopup>
</view>
</view>
</template>
<script>
import VanPopup from '@/wxcomponents/vant/popup/index'
export default {
components: {
VanPopup
},
props: {
info: {
type: Object,
default: {}
}
},
data () {
return {
popupIsShow: false,
infoData: this.info,
selectedSpecArr: [],
buyNum: 1
}
},
computed: {
selectedSpecKey () {
return this.selectedSpecArr.join()
},
curSelectedSpecValue () {
return this.infoData.productValue[this.selectedSpecKey]
},
disableMinus () {
return this.buyNum === 1
},
disablePlus () {
return this.buyNum >= this.curSelectedSpecValue.stock
}
},
created () {
this._initSpecSelectStatus()
},
methods: {
onPopupClose () {
this.popupIsShow = false
},
onPopupShow () {
this.popupIsShow = true
},
onSelectSpec (idx, idx2, spec) {
this.infoData['productAttr'][idx]['attr_value'].map((spec) => {
spec.check = false
return spec
})
this.infoData['productAttr'][idx]['attr_value'][idx2]['check'] = true
let tempSelectedSpecArr = JSON.parse(JSON.stringify(this.selectedSpecArr))
tempSelectedSpecArr[idx] = spec.attr
this.selectedSpecArr = tempSelectedSpecArr
},
onMinus () {
if (this.disableMinus) {
return false
}
this.buyNum--
},
onPlus () {
if (this.disablePlus) {
return false
}
this.buyNum++
},
onInputBuyNum (e) {
const value = e.detail.value
let rs = ''
const reg = /^\d*$/
for (let i = 0; i < value.length; i++) {
const char = value[i]
if (reg.test(char)) {
rs += char
}
}
if (rs) {
if (rs > this.curSelectedSpecValue.stock) {
rs = this.curSelectedSpecValue.stock
}
} else {
rs = 1
}
this.$nextTick(() => {
this.buyNum = rs
})
},
_initSpecSelectStatus () {
let tempSpecArr = []
for (let attr in this.infoData.productValue) {
tempSpecArr = attr.split(',')
break
}
this.selectedSpecArr = tempSpecArr
this.infoData.productAttr.map((attr, idx) => {
attr.attr_value.map((spec) => {
if (spec.attr === tempSpecArr[idx]) {
spec.check = true
}
return spec
})
return attr
})
}
}
}
</script>
<style lang="scss" scoped>
.specs-select {
background-color: #fff;
&-container {
.row {
display: flex;
align-items: center;
font-size: 26rpx;
color: #82848f;
height: 80rpx;
padding: 0 30rpx;
margin-top: 20rpx;
.content {
flex: 1;
font-size: 28rpx;
color: #282828;
}
.iconfont {
font-size: 28rpx;
color: #7a7a7a;
}
}
::v-deep .popup {
height: 800rpx;
.header {
position: relative;
display: flex;
padding: 0 0 30rpx;
margin-top: 28rpx;
.base-info {
display: flex;
flex-direction: column;
.name {
flex: 1;
width: 500rpx;
font-size: 32rpx;
color: #202020;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stock {
font-size: 24rpx;
color: #999;
margin-top: 40rpx;
.price {
font-size: 36rpx;
color: #fc4141;
text {
font-size: 24rpx;
}
}
}
}
.cover {
width: 150rpx;
min-width: 150rpx;
height: 150rpx;
}
.iconfont {
position: absolute;
right: 30rpx;
top: -4rpx;
font-size: 34rpx;
color: #8a8a8a;
}
}
.body {
.specs-scroll-view {
height: calc(800rpx - 192rpx);
.specs-list {
.specs-item {
margin-top: 36rpx;
&-title {
font-size: 30rpx;
color: #999;
padding: 0 30rpx;
}
&-values {
display: flex;
flex-wrap: wrap;
padding: 0 30rpx 0 16rpx;
view {
font-size: 26rpx;
color: #282828;
padding: 6rpx 32rpx;
border-radius: 24rpx;
margin: 20rpx 0 0 14rpx;
background-color: #f2f2f2;
border: 2rpx solid #f2f2f2;
&.on {
color: #e93323;
background: #fff4f3;
border-color: #e93323;
}
}
}
}
}
.stepper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
margin-top: 36rpx;
&-lable {
font-size: 30rpx;
color: #999;
}
&-field {
display: flex;
align-items: center;
height: 54rpx;
.iconfont {
display: flex;
align-items: center;
justify-content: center;
width: 84rpx;
height: 100%;
font-size: 24rpx;
&.disabled {
color: #dedede;
}
}
input {
width: 84rpx;
color: #282828;
font-size: 32rpx;
line-height: 1.4em;
text-align: center;
background: #f2f2f2;
}
}
}
}
}
}
}
}
</style>
2、构建产品详情页面Description.vue
<template>
<view class="description">
<view class="description-container">
<view class="title">产品介绍</view>
<rich-text :nodes="info"></rich-text>
</view>
</view>
</template>
<script>
export default {
props: {
info: {
type: String,
default: ''
}
}
}
</script>
<style lang="scss" scoped>
.description {
margin-top: 20rpx;
background-color: #fff;
&-container {
padding-bottom: 100rpx;
.title {
font-size: 30rpx;
color: #282828;
height: 86rpx;
width: 100%;
background-color: #fff;
text-align: center;
line-height: 86rpx;
}
}
}
</style>
3、在详情页面中导入组件goods_detail
<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 style="width: 100%"') : ''
},
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>
课程收获:
这节课主要学习到了,修改外部引入的框架使用::v-deep,然后在需要修改的组件上添加class,例如:::v-deep .goods-list 就可以直接修改引入的组件样式,第二种方法是直接找到组件的位置进行修改(组件引入少的情况下可以,复用多不适用),再有学习到了,对于外部直接生成的html标签,小程序是不支持的,需要通过进行引入,其中包括的参数有nodes(节点列表 / HTML String)、space(显示连续空格)、selectable(富文本是否可以长按选中,可用于复制,粘贴等场景)image-menu-prevent(阻止长按图片时弹起默认菜单(将该属性设置为image-menu-prevent或image-menu-prevent=“true”),只在初始化时有效,不能动态变更;若不想阻止弹起默认菜单,则不需要设置此属性)、preview(富文本中的图片是否可点击预览。在不设置的情况下,若 rich-text 未监听点击事件,则默认开启。未显示设置 preview 时会进行点击默认预览判断,建议显示设置 preview)最重要的是修改样式需要通过正则表达式进行替换例如img显示问题等