继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

【备战春招】第19天 商品详情页规格选择功能实现

imooc66
关注TA
已关注
手记 54
粉丝 2
获赞 23

课程章节: 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显示问题等

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP