前言
最近刚接手了一个小程序商城的项目(uniapp开发),我目前负责的是用户中心以及购物车模块。由于之前没有类似项目的经验,可能部分功能的代码实现不是很优雅,还望谅解和指出,谢谢。
现有条件:目前没有提供任何接口,也无UI设计图纸,只提供了一个线上的小程序作为参照
开发思路
- 造点假数据,先实现几个基本的功能:勾选功能,商品总数以及总价的计算。
- 勾选店铺时,旗下所有商品会被选中;当勾选中所有商品时,对应的店铺会被选中;全选以及取消全选的实现。
- 商品总数和总价的计算可以用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"> {{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"> {{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