课程名称: 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" :class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="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显示问题等