手撸一个购物车组件

前言

最近刚接手了一个小程序商城的项目(uniapp开发),我目前负责的是用户中心以及购物车模块。由于之前没有类似项目的经验,可能部分功能的代码实现不是很优雅,还望谅解和指出,谢谢。
在这里插入图片描述
现有条件:目前没有提供任何接口,也无UI设计图纸,只提供了一个线上的小程序作为参照

开发思路

  1. 造点假数据,先实现几个基本的功能:勾选功能,商品总数以及总价的计算。
  2. 勾选店铺时,旗下所有商品会被选中;当勾选中所有商品时,对应的店铺会被选中;全选以及取消全选的实现。
  3. 商品总数和总价的计算可以用conputed实现。

一,粗略设计了一下数据结构,关键点在于店铺及旗下商品都有checked属性,用来绑定checkbox状态的

list: [{
	id: 1,
	shopName: '湖北定制链',
	checked: false,
	productList: [{
			id: 2,
			name: '衣服一',
			checked: false,
			num: 1,
			price: '95.50',
			image: 'https://i.postimg.cc/0Q9BBXw0/20211014171328.jpg',
			title: 'LT404摇粒绒内胆三合一冲锋衣女款',
			style: '女款-湖蓝',
			size: 'S'
			}, {
			id: 3,
			name: '衣服二',
			checked: false,
			num: 2,
			price: '47.00',
			image: 'https://i.postimg.cc/4NGRH8MQ/20211014171323.jpg',
			title: 'GF2826摇粒绒一体冲锋衣',
			style: '男女同款-黄色',
			size: 'L'
			}]
	}]

二,布局设计,css部分太多了,这里就不贴了,在文章末尾会贴出全部代码。

<template>
	<view>
		<view class="products" v-for="(item,index) in list" :key="item.id">
			<view class="shop_name">
					<!-- 店铺名 -->
					<u-checkbox @change="checkboxChange" shape="circle" v-model="item.checked" :name="item.shopName">
						<text class="icon shop_icon">&#xe6cb; {{item.shopName}}</text>
					</u-checkbox>
			</view>
			<view class="product_list" v-for="child in item.productList" :key="child.id">
				<!-- 商品详情 -->
				<u-checkbox class="item_check" @change="productChange" shape="circle" v-model="child.checked"
					:name="child.namename">

				</u-checkbox>
				<view class="product_item">
					<view class="product_detail">
						<view class="product_img">
							<!-- 商品图片 -->
							<image :src="child.image"></image>
						</view>
						<view class="product_desc">
							<view class="product_title" @click="to('/pages/goods')">
								<!-- 商品标题 -->
								{{child.title}}
							</view>
							<view class="product_style">
								<text>{{child.style}}</text>
								<u-icon name="arrow-down"></u-icon>
							</view>
							<view class="product_price">
								<text class="price_info">
									¥
									<text class="price">{{child.price}}</text>
								</text>
								<u-number-box :min="1" v-model="child.num"></u-number-box>
							</view>
						</view>
					</view>
				</view>
			</view>
		</view>
		<view class="product_order">
			<view class="order_total">
				<view v-if="!isEdit">
					<view>
						<text class="total_price">总计</text>
						<text class="total_product">¥{{getTotalPrice}}</text>
					</view>
					<text class="tips">{{total}}件,不含运费</text>
				</view>
				<view v-else>
					<!-- 全选 -->
					<u-checkbox @change="checkAll" shape="circle" v-model="isCheckedAll">
						<text >全选</text>
					</u-checkbox>
				</view>
			</view>
			<view class="order_submit">
				<text v-if="!isEdit" @click="subOrder">提交订单</text>
				<text v-else @click="deleteProducts">删除</text>
			</view>
		</view>
	</view>
</template>

三,选中/取消选中店铺时,遍历该店铺下的所有商品的checked属性,将其修改为true(选中)/false(取消选中)。选中/取消选中商品时,修改店铺的checked属性。

methods:{
checkboxChange(e) {
		let index = this.getIndex(e.name)
		// (取消)勾选店铺时,同步修改店铺下所有商品的状态为(取消)勾选
		this.list[index].productList.forEach((n) => n.checked = e.value)
		},
productChange(e) {
		//勾选商品之后,判断所属店铺下的商品勾选状态,如果商品状态全部为选中,则店铺亦需被选中
		this.$nextTick(() => {
			this.list.forEach(item => {
				item.checked = item.productList.every(n => n.checked)
					})
				})
		},
getIndex(name) {
		// 根据商品名获取对应的index
		let index
		this.list.map((item, i) => {
			item.shopName === name && (index = i)
			})
		return index
		},
}

四,计算商品总数量,以及商品总价。

computed: {
			//获取商品总价
			getTotalPrice() {
				let result = 0
				this.total = 0
				this.list.map(item => {
					item.productList.map(n => {
						// 计算选中商品总价和总数量
						n.checked && (result += Number(n.price) * n.num) && (this.total += n.num)
					})
				})
				if(this.isEdit){
					// 当编辑商品时,若已勾选所有店铺,则全选按钮应该被选中,否则反之
					this.isCheckedAll = this.list.every(n=>n.checked)
				}
				//格式化总价
				return priceFormat(result)
			}
		},

在此,一个简单的购物车功能就实现啦!

尾声:在此处贴出全部源码

<template>
	<view>
		<view class="products" v-for="(item,index) in list" :key="item.id">
			<view class="shop_name">
					<!-- 店铺名 -->
					<u-checkbox @change="checkboxChange" shape="circle" v-model="item.checked" :name="item.shopName">
						<text class="icon shop_icon">&#xe6cb; {{item.shopName}}</text>
					</u-checkbox>
			</view>
			<view class="product_list" v-for="child in item.productList" :key="child.id">
				<!-- 商品详情 -->
				<u-checkbox class="item_check" @change="productChange" shape="circle" v-model="child.checked"
					:name="child.namename">

				</u-checkbox>
				<view class="product_item">
					<view class="product_detail">
						<view class="product_img">
							<!-- 商品图片 -->
							<image :src="child.image"></image>
						</view>
						<view class="product_desc">
							<view class="product_title" @click="to('/pages/goods')">
								<!-- 商品标题 -->
								{{child.title}}
							</view>
							<view class="product_style">
								<text>{{child.style}}</text>
								<u-icon name="arrow-down"></u-icon>
							</view>
							<view class="product_price">
								<text class="price_info">
									¥
									<text class="price">{{child.price}}</text>
								</text>
								<u-number-box :min="1" v-model="child.num"></u-number-box>
							</view>
						</view>
					</view>
				</view>
			</view>
		</view>
		<view class="product_order">
			<view class="order_total">
				<view v-if="!isEdit">
					<view>
						<text class="total_price">总计</text>
						<text class="total_product">¥{{getTotalPrice}}</text>
					</view>
					<text class="tips">{{total}}件,不含运费</text>
				</view>
				<view v-else>
					<!-- 全选 -->
					<u-checkbox @change="checkAll" shape="circle" v-model="isCheckedAll">
						<text >全选</text>
					</u-checkbox>
				</view>
			</view>
			<view class="order_submit">
				<text v-if="!isEdit" @click="subOrder">提交订单</text>
				<text v-else @click="deleteProducts">删除</text>
			</view>
		</view>
	</view>
</template>

<script>
	import priceFormat from '../libs/priceFormat.js'
	export default {
		name: "cartProducts",
		props:{
			isEdit:{
				type:Boolean,
				default:false
			}
		},
		data() {
			return {
				list: [{
					id: 1,
					shopName: '湖北定制链',
					checked: false,
					productList: [{
						id: 2,
						name: '衣服一',
						checked: false,
						num: 1,
						price: '95.50',
						image: 'https://i.postimg.cc/0Q9BBXw0/20211014171328.jpg',
						title: 'LT404摇粒绒内胆三合一冲锋衣女款',
						style: '女款-湖蓝',
						size: 'S'
					}, {
						id: 3,
						name: '衣服二',
						checked: false,
						num: 2,
						price: '47.00',
						image: 'https://i.postimg.cc/4NGRH8MQ/20211014171323.jpg',
						title: 'GF2826摇粒绒一体冲锋衣',
						style: '男女同款-黄色',
						size: 'L'
					}]
				}, {
					id: 4,
					shopName: '河南定制链',
					checked: false,
					productList: [{
						id: 5,
						name: '衣服一',
						checked: false,
						num: 1,
						price: '95.50',
						image: 'https://i.postimg.cc/0Q9BBXw0/20211014171328.jpg',
						title: 'LT404摇粒绒内胆三合一冲锋衣女款',
						style: '女款-湖蓝',
						size: 'S'
					}]
				}],
				total: 0,//当前选中商品的数量
				isCheckedAll:false,//全选按钮状态
			};
		},
		methods: {
			checkboxChange(e) {
				let index = this.getIndex(e.name)
				// (取消)勾选店铺时,同步修改店铺下所有商品的状态为(取消)勾选
				this.list[index].productList.forEach((n) => n.checked = e.value)
			},
			productChange(e) {
				//勾选商品之后,判断所属店铺下的商品勾选状态,如果商品状态全部为选中,则店铺亦需被选中
				this.$nextTick(() => {
					this.list.forEach(item => {
						item.checked = item.productList.every(n => n.checked)
					})
				})
			},
			getIndex(name) {
				// 根据商品名获取对应的index
				let index
				this.list.map((item, i) => {
					item.shopName === name && (index = i)
				})
				return index
			},
			checkAll(e){
				this.list.forEach(item=>{
					//全部(取消)选中
					item.checked=!this.isCheckedAll
					item.productList.forEach(child=>child.checked=!this.isCheckedAll)
				})
			},
			deleteProducts(){
				console.log('删除商品')
			},
			subOrder(){
				console.log('提交订单')
			}
		},
		computed: {
			//获取商品总价
			getTotalPrice() {
				let result = 0
				this.total = 0
				this.list.map(item => {
					item.productList.map(n => {
						// 计算选中商品总价和总数量
						n.checked && (result += Number(n.price) * n.num) && (this.total += n.num)
					})
				})
				if(this.isEdit){
					// 当编辑商品时,若已勾选所有店铺,则全选按钮应该被选中,否则反之
					this.isCheckedAll = this.list.every(n=>n.checked)
				}
				//格式化总价
				return priceFormat(result)
			}
		},
		watch:{
			isEdit(newVal,oldVal){
				this.list.forEach(item=>{
					item.checked=false
					item.productList.forEach(child=>child.checked=false)
				})
			}
		}
	}
</script>

<style lang="less">
	@font-face {
		font-family: 'iconfont';
		src: url('https://at.alicdn.com/t/font_2875153_yvvbkl11t2l.ttf?t=1634542907758') format('truetype');
	}

	.icon {
		font-family: iconfont;
		color: #000;
		font-size: 26rpx;
	}

	.product_list {
		display: flex;
		align-items: center;

		.product_item {
			flex-shrink: 0;
		}
	}

	/deep/.item_check {
		width: 50rpx;
	}

	.products:last-child {
		margin-bottom: 100px;
	}

	.products {
		margin: 20rpx;
		padding: 20rpx;
		background-color: #fff;
		border-radius: 10rpx;

		.shop_name {
			font-weight: 700;
			padding-bottom: 12rpx;
			border-bottom: 2rpx solid #f6f6f6;

			.shop_icon {
				padding-left: 10rpx;
			}
		}

		.product_detail {
			margin-top: 20rpx;
			display: flex;

			.product_img {
				flex-shrink: 0;
				width: 200rpx;
				height: 200rpx;

				//margin-right: 20rpx;
				image {
					width: 100%;
					height: 100%;
				}
			}

			.product_desc {
				width: 400rpx;
				margin-left: 20rpx;

				.product_title {
					line-height: 140%;
					height: 80rpx;
					overflow: hidden;
					text-overflow: ellipsis;
					display: -webkit-box;
					-webkit-box-orient: vertical;
					-webkit-line-clamp: 2;
				}

				.product_style {
					width: 100%;
					background-color: #f6f6f6;
					font-size: 24rpx;
					font-weight: 700;
					color: #000;
					padding: 0 14rpx;
					margin-top: 20rpx;
					height: 46rpx;
					line-height: 46rpx;
					display: flex;
					justify-content: space-between;
				}

				.product_price {
					display: flex;
					justify-content: space-between;
					margin-top: 20rpx;

					.price_info {
						color: red;
						font-size: 22rpx;

						.price {
							font-size: 28rpx;
							font-weight: 700;
						}
					}

					.num {
						color: #3C3C3C;
						font-size: 24rpx;
						font-weight: 700;
					}
				}
			}
		}

		.product_size {
			width: 100%;
			height: 60rpx;
			line-height: 60rpx;
			padding-left: 10rpx;
			background-color: #F6F6F6;
			margin-top: 10rpx;
			display: flex;
			justify-content: space-between;
		}
	}
	.product_order{
		position: fixed;
		bottom: 49px;
		left: 0;
		z-index: 9999;
		background-color: #fff;
		width: 100%;
		display: flex;
		justify-content: space-between;
		align-items: center;
		height: 70rpx;
	}
	.order_submit{
		width: 160rpx;
		background-color: #FF334E;
		color: #fff;
		line-height: 70rpx;
		text-align: center;
	}
	.order_total{
		margin-left: 20rpx;
	}
	.total_price{
		color: #000;
		font-size: 24rpx;
	}
	.total_product{
		color: #FE344C;
	}
	.tips{
		color: #AAAAAA;
		font-size: 22rpx;
	}
</style>

priceFormat.js

const priceFormat = value => {
	if (!value) return '0.00'
	value = value.toFixed(2)
	var intPart = Math.trunc(value) // 获取整数部分
	var intPartFormat = intPart.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') // 将整数部分逢三一断
	var floatPart = '.00' // 预定义小数部分
	var value2Array = value.split('.')
	// =2表示数据有小数位
	if (value2Array.length === 2) {
		floatPart = value2Array[1].toString() // 拿到小数部分
		if (floatPart.length === 1) {
			return intPartFormat + '.' + floatPart + '0'
		} else {
			return intPartFormat + '.' + floatPart
		}
	} else {
		return intPartFormat + floatPart
	}
}

export default priceFormat

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值