任务四 工单2
最终效果如图:


1、开发团队成员需要对商品的选择添加减少进行逻辑研发,如图4.2.3中icon交互。商品goods属性use_property为1的商品是直接增减的,不需要选择规格,通过computed()计算属性goodCartNum(good.id)获取该商品在购物车的数量(goodCartNum()方法代码如下)。其中this.cart为购物车数组,用于顾客商品装载,如果购物车中没有该商品则返回0。

其中reduce是一个数组的方法,它允许把数组中的值缩减为一个值。在Vue中,reduce经常用于处理数组和对象。
computed: {
...mapState(['orderType','store','address']),//引用数据
...mapGetters(['isLogin']),
goodCartNum() { //计算单个饮品添加到购物车的数量
return (id) => this.cart.reduce((acc, cur) => { //acc累加器cur 当前元素的值
if(cur.id === id) { //通过id到购物车中找到此商品
return acc += cur.number //计算该商品的个数
}
return acc
}, 0)
}
...
...
商品数量数据显示完成后,将对商品个数添加、减少进行交互。通过@tap="handleAddToCart(item, good, 1)"方法执行添加,传入当前种类信息、商品信息、个数参数,方法代码如下:
handleAddToCart(cate, good, num) { //添加到购物车
const index = this.cart.findIndex(item => {//findIndex() 方法返回传入一个条件(函数)符合条件的数组第一个元素位置,如果没有符合条件的元素返回-1
if(good.use_property) {//如果是选规格的
return (item.id === good.id) && (item.props_text === good.props_text)
} else {//不选规格的
return item.id === good.id
}
})
if(index > -1) {
this.cart[index].number += num //如果对象已有,直接在已有的对象上加1
} else {
this.cart.push({//否则 重新添加进购物车
id: good.id,
...
...
})
}
}
其中,findIndex方法用于查找数组中满足特定条件的第一个元素的索引位置。在购物车cart数组所对应的索引中找到此商品进行添加数量,反之,购物车未找到该商品索引返回默认值-1,通过push方法将新商品信息推送至购物车数组中。
添加商品个数已完成,需对减少商品个数进行操作。通过@tap="handleReduceFromCart(item, good)"方法进行商品减少逻辑,利用this.cart.findIndex(item => item.id === good.id)方法找到索引。若减少数量为0了,通过this.cart.splice(index, 1)将此商品清除,效果图如图4.2.4。

handleReduceFromCart(item, good) {//减少数量
const index = this.cart.findIndex(item => item.id === good.id)
this.cart[index].number -= 1
if(this.cart[index].number <= 0) {
this.cart.splice(index, 1)//如果减少到了0,则剔除该商品
}
}
menu.vue 商品增减逻辑 完整代码
<template>
<view class="container">
<view class="main">
<!-- 头部 -->
<view class="nav">
<view class="header">
<!-- 未接单 如果是自取 ,默认自取 显示商家距离 -->
<view class="left" v-if="orderType == 'takein'">
<view class="store-name">
<text>{{ store.name }}</text>
<view class="iconfont iconarrow-right"></view>
</view>
<view class="store-location">
<image src='/static/images/order/location.png' style="width: 30rpx; height: 30rpx;"
class="mr-10"></image>
<text>距离您 {{ store.distance_text}} 米</text>
</view>
</view>
<!-- 如果是外卖 ,显示街道 -->
<view class="left overflow-hidden" v-else>
<view class="d-flex align-items-center overflow-hidden">
<image src='/static/images/order/location.png' style="width: 30rpx; height: 30rpx;"
class="mr-10"></image>
<view class="font-size-extra-lg text-color-base font-weight-bold text-truncate">
{{ address.street }}
</view>
</view>
<view class="font-size-sm text-color-assist overflow-hidden text-truncate">
由<text class="text-color-base" style="margin: 0 10rpx">{{ store.name }}</text>配送
</view>
</view>
<!-- 已接单 点击后样式变化 -->
<view class="right">
<view class="dinein" :class="{active: orderType == 'takein'}" @tap="SET_ORDER_TYPE('takein')">
<text>自取</text>
</view>
<view class="takeout" :class="{active: orderType == 'takeout'}" @tap="takout">
<text>外卖</text>
</view>
</view>
</view>
<view class="coupon">
<text class="title">"霸气mini卡"超级购券活动,赶紧去购买</text>
<view class="iconfont iconarrow-right"></view>
</view>
</view>
<!-- 中间左侧 -->
<view class="content">
<scroll-view class="menus" scroll-with-animation scroll-y>
<view class="wrapper">
<view class="menu" :id="`menu-${item.id}`" :class="{'current': item.id === currentCateId}"
@tap="handleMenuTap(item.id)" v-for="(item, index) in goods" :key="index">
<text>{{ item.name }}</text>
</view>
</view>
</scroll-view>
<!-- 中间右侧 -->
<scroll-view class="goods" scroll-with-animation scroll-y :scroll-top="cateScrollTop"
@scroll="handleGoodsScroll">
<view class="wrapper">
<!-- 轮播图 -->
<swiper class="ads" id="ads" autoplay :interval="3000" indicator-dots>
<swiper-item v-for="(item, index) in ads" :key='index'>
<image :src="item.image"></image>
</swiper-item>
</swiper>
<!-- 商品列表 -->
<view class="list">
<!-- category begin -->
<view class="category" v-for="(item, index) in goods" :key="index" :id="`cate-${item.id}`">
<!-- 大标题 系列 -->
<view class="title">
<text>{{ item.name }}</text>
<image :src="item.icon" class="icon"></image>
</view>
<view class="items">
<!-- 每一个商品 begin -->
<view class="good" v-for="(good, key) in item.goods_list" :key="key">
<image :src="good.images" class="image" @tap="showGoodDetailModal(item, good)">
</image>
<view class="right">
<text class="name">{{ good.name }}</text>
<text class="tips">{{ good.content }}</text>
<view class="price_and_action">
<text class="price">¥{{ good.price }}</text>
<!-- 选规格 use_property 1 -->
<view class="btn-group" v-if="good.use_property">
<button type="primary" class="btn property_btn" hover-class="none"
size="mini" @tap="showGoodDetailModal(item, good)">
选规格
</button>
</view>
<!-- 无选规格 use_property 0 正常增减数量-->
<view class="btn-group" v-else>
<!-- 减少 -->
<button type="default" plain class="btn reduce_btn" size="mini"
hover-class="none" v-show="goodCartNum(good.id)"
@tap="handleReduceFromCart(item, good)">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number" v-show="goodCartNum(good.id)">
{{ goodCartNum(good.id) }}
</view>
<!-- 增加 -->
<button type="primary" class="btn add_btn" size="min"
hover-class="none" @tap="handleAddToCart(item, good, 1)">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
</view>
</view>
<!-- 商品 end -->
</view>
</view>
<!-- category end -->
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 商品详情模态框 begin -->
<modal :show="goodDetailModalVisible" class="good-detail-modal" color="#5A5B5C" width="90%" custom
padding="0rpx" radius="12rpx">
<!-- 上方介绍 -->
<view class="cover">
<image v-if="good.images" :src="good.images" class="image"></image>
<view class="btn-group">
<image src="/static/images/menu/share-good.png"></image>
<image src="/static/images/menu/close.png" @tap="closeGoodDetailModal"></image>
</view>
</view>
<scroll-view class="detail" scroll-y>
<view class="wrapper">
<!-- 商品名称 -->
<view class="basic">
<view class="name">{{ good.name }}</view>
<view class="tips">{{ good.content }}</view>
</view>
<!-- 选规格 种类-->
<view class="properties" v-if="good.use_property">
<view class="property" v-for="(item, index) in good.property" :key="index">
<!-- 标题 -->
<view class="title">
<text class="name">{{ item.name }}</text>
<view class="desc" v-if="item.desc">({{ item.desc }})</view>
</view>
<!--选规格 内容 -->
<view class="values">
<view class="value" v-for="(value, key) in item.values" :key="key"
:class="{'default': value.is_default}" @tap="changePropertyDefault(index, key)">
{{ value.value }}
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="action">
<!-- 左侧金额 -->
<view class="left">
<view class="price">¥{{ good.price }}</view>
<!-- 选规格的详细信息 -->
<view class="props" v-if="getGoodSelectedProps(good)">
{{ getGoodSelectedProps(good) }}
</view>
</view>
<!-- 右侧 选择数量 -->
<view class="btn-group">
<button type="default" plain class="btn" size="mini" hover-class="none">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number">{{ good.number }}</view>
<button type="primary" class="btn" size="min" hover-class="none">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
<!-- 下方 加入购物车 按钮 -->
<view class="add-to-cart-btn">
<view>加入购物车</view>
</view>
</modal>
<!-- 商品详情模态框 end -->
</view>
</template>
<script>
import {
mapState,
mapActions,
mapGetters,
mapMutations
} from 'vuex'
import modal from '@/components/modal/modal'
export default {
components: {
modal
},
data() {
return {
goods: [], //所有商品
currentCateId: 6905, //默认分类
cateScrollTop: 0, //scroll-view滑动刻度
goodDetailModalVisible: false, //是否饮品详情模态框
good: {}, //当前饮品
cart: [], //购物车
ads:[{image: 'https://s3.uuu.ovh/imgs/2025/04/06/66a62739b6bb6c6e.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/5202f8ff24613d35.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/96228fabd6c0610c.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/a69f605a1dcc3496.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/9a0632bfa5685857.png'}
], //轮播图数据
}
},
computed: {
...mapState(['orderType', 'store', 'address']), //引用数据
...mapGetters(['isLogin']),
goodCartNum() { //计算单个饮品添加到购物车的数量
return (id) => this.cart.reduce((acc, cur) => { // (acc) (累加器) (cur) (当前元素的值)
if (cur.id === id) { //通过id到购物车中找到此商品
return acc += cur.number //计算
}
return acc
}, 0)
},
},
async onLoad() {
await this.init() //在生命周期调用初始化页面
},
methods: {
...mapMutations(['SET_ORDER_TYPE']), //点击自取,设置order_type的参数
...mapActions(['getStore']), //引入getStore方法
async init() { //页面初始化方法
await this.getStore() //调用getStore方法,对store变量进行商家数据填充
this.goods = await this.$api('goods') //对goods变量进行货物数据填充
},
takout() { //点击外卖
if (this.orderType == 'takeout') return //如果是选取外卖状态,则返回
if (!this.isLogin) { //未登录
uni.navigateTo({
url: '/pages/login/login'
})
return
}
uni.navigateTo({ //跳转地址页面
url: '/pages/address/address'
})
},
handleGoodsScroll({
detail
}) { //商品列表滚动事件
if (!this.sizeCalcState) {
this.calcSize() //为每个item添加一个top高度
}
const {
scrollTop
} = detail //detail为参数,可以查看scrollTop,根据scrollTop来联动左侧item
let tabs = this.goods.filter(item => item.top <= scrollTop).reverse() //过滤、排序,
if (tabs.length > 0) {
this.currentCateId = tabs[0].id //定义currentCateId 如果跟item.id一样,则使用:class
}
},
calcSize() {
let h = 10
//该方法位获取轮播图的高度
let view = uni.createSelectorQuery().select('#ads') //获取轮播图节点
view.fields({
size: true
}, data => {
h += Math.floor(data.height) //赋予h参数轮播图高度
}).exec()
//获取每个item的高度,叠加,装入数据里面
this.goods.forEach(item => {
let view = uni.createSelectorQuery().select(`#cate-${item.id}`) //每个商品view
view.fields({
size: true
}, data => {
item.top = h //为每个item设置一个top属性
h += data.height //对h属性增加一个item高度
item.bottom = h //设置item的底部bottom属性
}).exec()
})
this.sizeCalcState = true
},
handleMenuTap(id) { //点击菜单项事件
if (!this.sizeCalcState) {
this.calcSize() //为每个item添加一个top高度
}
this.currentCateId = id
this.$nextTick(() => this.cateScrollTop = this.goods.find(item => item.id == id).top) //
},
showGoodDetailModal(item, good) { //打开商品详情
this.good = JSON.parse(JSON.stringify({
...good,
number: 1
})) //当前饮品赋予good值,多加一个属性number
this.goodDetailModalVisible = true
},
closeGoodDetailModal() { //关闭饮品详情模态框
this.goodDetailModalVisible = false
this.good = {} //清空
},
getGoodSelectedProps(good, type = 'text') { //计算当前饮品所选属性 文字
if (good.use_property) {
let props = []
good.property.forEach(({
values
}) => {
values.forEach(value => {
if (value.is_default) {
props.push(type === 'text' ? value.value : value.id)
}
})
})
return type === 'text' ? props.join(',') : props
}
return ''
},
changePropertyDefault(index, key) { //改变默认属性值
this.good.property[index].values.forEach(value => this.$set(value, 'is_default', 0)) //遍历每个is_default值为0
this.good.property[index].values[key].is_default = 1 //点击后 改为 1 做状态调整
this.good.number = 1 //点击后该商品数量为1
},
handleAddToCart(cate, good, num) { //添加到购物车 +1
const index = this.cart.findIndex(item => { //findIndex() 方法返回传入一个条件(函数)符合条件的数组第一个元素位置,如果没有符合条件的元素返回 -1
if (good.use_property) { //如果是选规格的
return (item.id === good.id) && (item.props_text === good.props_text)
} else {
return item.id === good.id
}
})
if (index > -1) {
this.cart[index].number += num //如果对象已有,直接在已有的对象上加1
} else {
this.cart.push({ //否则 重新添加进购物车
id: good.id,
cate_id: cate.id,
name: good.name,
price: good.price,
number: num,
image: good.images,
use_property: good.use_property,
props_text: good.props_text,
props: good.props
})
}
},
handleReduceFromCart(item, good) { //减少商品 -1
const index = this.cart.findIndex(item => item.id === good.id)
this.cart[index].number -= 1
if (this.cart[index].number <= 0) {
this.cart.splice(index, 1) //如果减少到了0,则剔除该商品
}
},
}
}
</script>
<style lang="scss" scoped>
@import '~@/pages/menu/menu.scss';
</style>
2、若购物车cart中有商品,通过cart.length > 0判断显示与隐藏购物车栏,购物车栏中涵盖购物车商品总数(getCartGoodsNumber)、商品合计金额(getCartGoodsPrice)等信息,menu.scss中完善.cart-box样式,效果如图4.2.5。相关页面代码如下:
<!-- 购物车栏 begin -->
<view class="cart-box" v-if="cart.length > 0">
<view class="mark">
<image src="/static/images/menu/cart.png" class="cart-img" @tap="openCartPopup"></image>
<view class="tag">{{ getCartGoodsNumber }}</view>
</view>
<view class="price">¥{{ getCartGoodsPrice }}</view>
<button type="primary" class="pay-btn" @tap="toPay" :disabled="disabledPay">
{{ disabledPay ? `差${spread}元起送` : '去结算' }}
</button>
</view>
<!-- 购物车栏 end -->

getCartGoodsNumber()、getCartGoodsPrice()、disabledPay()、spread()为计算属性。前两者使用reduce()方法计算购物车中所有商品个数和金额,disabledPay()为达到起送价方法,spread()为若顾客选择方式为外卖配送,判断起送金额的方法。具体方法如下:
getCartGoodsNumber() { //计算购物车总数
return this.cart.reduce((acc, cur) => acc + cur.number, 0)
},
getCartGoodsPrice() { //计算购物车总价
return this.cart.reduce((acc, cur) => acc + cur.number * cur.price, 0)
},
disabledPay() { //是否达到起送价
return this.orderType == 'takeout' && (this.getCartGoodsPrice < this.store.min_price) ? true : false
},
spread() { //差多少元起送
if(this.orderType != 'takeout') return
return parseFloat((this.store.min_price - this.getCartGoodsPrice).toFixed(2))
},
3、对于商品goods属性use_property为0的商品,通过点击“选规格”点击交互后,需在模态框中完成数量增加、减少及加入购物车按钮功能(use_property为1的商品也可在模态框完成此功能),如图4.2.6。

模态框中通过handlePropertyReduce()、handlePropertyAdd()、handleAddToCartInModal()方法完成。Object.assign()此方法是浅拷贝的,即只复制对象的引用,而不是对象本身。方法代码如下:
handlePropertyAdd() {//模态框中加一个商品
this.good.number += 1
},
handlePropertyReduce() {//模态框中减一个商品
if(this.good.number === 1) return //如果数量为1返回不操作
this.good.number -= 1
},
handleAddToCartInModal() {//模态框中加入购物车
const product = Object.assign({}, this.good, {props_text: this.getGoodSelectedProps(this.good), props: this.getGoodSelectedProps(this.good, 'id')})//重新组装一个对象
this.handleAddToCart(this.category, product, this.good.number)//添加到购物车中
this.closeGoodDetailModal()//关闭模态框
},
this.category为该商品所在的种类,因此在数据中添加当前饮品所在分类category对象,在打开模态框时,通过JSON.parse(JSON.stringify(item))赋值category以便显示种类数量;关闭模态框时,通过this.category = {}方法完成种类清楚。
模态框中增减、下单功能完成后,通过goodCartNum()方法获取个数,在图4.2.7中显示具体个数。


4、当购物车中有相关商品后,左侧菜单栏将显示已选择此种类的个数总和,通过menuCartNum()计算属性完成,如图4.2.8所示。
<!-- 加入购物车后 -->
<view class="dot" v-show="menuCartNum(item.id)">{{ menuCartNum(item.id) }}</view>
...
...
menuCartNum() {//每一栏的数量,进行判断赛选
return (id) => this.cart.reduce((acc, cur) => {
if(cur.cate_id === id) {
return acc += cur.number
}
return acc
}, 0)
}
menu.vue 购物车栏状态 完整代码
<template>
<view class="container">
<view class="main">
<!-- 头部 -->
<view class="nav">
<view class="header">
<!-- 未接单 如果是自取 ,默认自取 显示商家距离 -->
<view class="left" v-if="orderType == 'takein'">
<view class="store-name">
<text>{{ store.name }}</text>
<view class="iconfont iconarrow-right"></view>
</view>
<view class="store-location">
<image src='/static/images/order/location.png' style="width: 30rpx; height: 30rpx;"
class="mr-10"></image>
<text>距离您 {{ store.distance_text}} 米</text>
</view>
</view>
<!-- 如果是外卖 ,显示街道 -->
<view class="left overflow-hidden" v-else>
<view class="d-flex align-items-center overflow-hidden">
<image src='/static/images/order/location.png' style="width: 30rpx; height: 30rpx;"
class="mr-10"></image>
<view class="font-size-extra-lg text-color-base font-weight-bold text-truncate">
{{ address.street }}
</view>
</view>
<view class="font-size-sm text-color-assist overflow-hidden text-truncate">
由<text class="text-color-base" style="margin: 0 10rpx">{{ store.name }}</text>配送
</view>
</view>
<!-- 已接单 点击后样式变化 -->
<view class="right">
<view class="dinein" :class="{active: orderType == 'takein'}" @tap="SET_ORDER_TYPE('takein')">
<text>自取</text>
</view>
<view class="takeout" :class="{active: orderType == 'takeout'}" @tap="takout">
<text>外卖</text>
</view>
</view>
</view>
<view class="coupon">
<text class="title">"霸气mini卡"超级购券活动,赶紧去购买</text>
<view class="iconfont iconarrow-right"></view>
</view>
</view>
<!-- 中间左侧 -->
<view class="content">
<scroll-view class="menus" scroll-with-animation scroll-y>
<view class="wrapper">
<view class="menu" :id="`menu-${item.id}`" :class="{'current': item.id === currentCateId}"
@tap="handleMenuTap(item.id)" v-for="(item, index) in goods" :key="index">
<text>{{ item.name }}</text>
<!-- 加入购物车后 -->
<view class="dot" v-show="menuCartNum(item.id)">{{ menuCartNum(item.id) }}</view>
</view>
</view>
</scroll-view>
<!-- 中间右侧 -->
<scroll-view class="goods" scroll-with-animation scroll-y :scroll-top="cateScrollTop"
@scroll="handleGoodsScroll">
<view class="wrapper">
<!-- 轮播图 -->
<swiper class="ads" id="ads" autoplay :interval="3000" indicator-dots>
<swiper-item v-for="(item, index) in ads" :key='index'>
<image :src="item.image"></image>
</swiper-item>
</swiper>
<!-- 商品列表 -->
<view class="list">
<!-- category begin -->
<view class="category" v-for="(item, index) in goods" :key="index" :id="`cate-${item.id}`">
<!-- 大标题 系列 -->
<view class="title">
<text>{{ item.name }}</text>
<image :src="item.icon" class="icon"></image>
</view>
<view class="items">
<!-- 每一个商品 begin -->
<view class="good" v-for="(good, key) in item.goods_list" :key="key">
<image :src="good.images" class="image" @tap="showGoodDetailModal(item, good)">
</image>
<view class="right">
<text class="name">{{ good.name }}</text>
<text class="tips">{{ good.content }}</text>
<view class="price_and_action">
<text class="price">¥{{ good.price }}</text>
<!-- 选规格 use_property 1 -->
<view class="btn-group" v-if="good.use_property">
<button type="primary" class="btn property_btn" hover-class="none"
size="mini" @tap="showGoodDetailModal(item, good)">
选规格
</button>
<view class="dot" v-show="goodCartNum(good.id)">{{ goodCartNum(good.id) }}</view>
</view>
<!-- 无选规格 use_property 0 正常增减数量-->
<view class="btn-group" v-else>
<!-- 减少 -->
<button type="default" plain class="btn reduce_btn" size="mini"
hover-class="none" v-show="goodCartNum(good.id)"
@tap="handleReduceFromCart(item, good)">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number" v-show="goodCartNum(good.id)">
{{ goodCartNum(good.id) }}
</view>
<!-- 增加 -->
<button type="primary" class="btn add_btn" size="min"
hover-class="none" @tap="handleAddToCart(item, good, 1)">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
</view>
</view>
<!-- 商品 end -->
</view>
</view>
<!-- category end -->
</view>
</view>
</scroll-view>
</view>
<!-- 购物车栏 begin -->
<view class="cart-box" v-if="cart.length > 0">
<view class="mark">
<image src="/static/images/menu/cart.png" class="cart-img" @tap="openCartPopup"></image>
<view class="tag">{{ getCartGoodsNumber }}</view>
</view>
<view class="price">¥{{ getCartGoodsPrice }}</view>
<button type="primary" class="pay-btn" @tap="toPay" :disabled="disabledPay">
{{ disabledPay ? `差${spread}元起送` : '去结算' }}
</button>
</view>
<!-- 购物车栏 end -->
</view>
<!-- 商品详情模态框 begin -->
<modal :show="goodDetailModalVisible" class="good-detail-modal" color="#5A5B5C" width="90%" custom
padding="0rpx" radius="12rpx">
<!-- 上方介绍 -->
<view class="cover">
<image v-if="good.images" :src="good.images" class="image"></image>
<view class="btn-group">
<image src="/static/images/menu/share-good.png"></image>
<image src="/static/images/menu/close.png" @tap="closeGoodDetailModal"></image>
</view>
</view>
<scroll-view class="detail" scroll-y>
<view class="wrapper">
<!-- 商品名称 -->
<view class="basic">
<view class="name">{{ good.name }}</view>
<view class="tips">{{ good.content }}</view>
</view>
<!-- 选规格 种类-->
<view class="properties" v-if="good.use_property">
<view class="property" v-for="(item, index) in good.property" :key="index">
<!-- 标题 -->
<view class="title">
<text class="name">{{ item.name }}</text>
<view class="desc" v-if="item.desc">({{ item.desc }})</view>
</view>
<!--选规格 内容 -->
<view class="values">
<view class="value" v-for="(value, key) in item.values" :key="key"
:class="{'default': value.is_default}" @tap="changePropertyDefault(index, key)">
{{ value.value }}
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="action">
<!-- 左侧金额 -->
<view class="left">
<view class="price">¥{{ good.price }}</view>
<!-- 选规格的详细信息 -->
<view class="props" v-if="getGoodSelectedProps(good)">
{{ getGoodSelectedProps(good) }}
</view>
</view>
<!-- 右侧 选择数量 -->
<view class="btn-group">
<button type="default" plain class="btn" size="mini" hover-class="none" @tap="handlePropertyReduce">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number">{{ good.number }}</view>
<button type="primary" class="btn" size="min" hover-class="none" @tap="handlePropertyAdd">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
<!-- 下方 加入购物车 按钮 -->
<view class="add-to-cart-btn" @tap="handleAddToCartInModal">
<view>加入购物车</view>
</view>
</modal>
<!-- 商品详情模态框 end -->
</view>
</template>
<script>
import {
mapState,
mapActions,
mapGetters,
mapMutations
} from 'vuex'
import modal from '@/components/modal/modal'
export default {
components: {
modal
},
data() {
return {
goods: [], //所有商品
currentCateId: 6905, //默认分类
cateScrollTop: 0, //scroll-view滑动刻度
goodDetailModalVisible: false, //是否饮品详情模态框
good: {}, //当前饮品
cart: [], //购物车
category: {}, //当前饮品所在分类
ads:[{image: 'https://s3.uuu.ovh/imgs/2025/04/06/66a62739b6bb6c6e.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/5202f8ff24613d35.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/96228fabd6c0610c.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/a69f605a1dcc3496.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/9a0632bfa5685857.png'}
], //轮播图数据
}
},
computed: {
...mapState(['orderType', 'store', 'address']), //引用数据
...mapGetters(['isLogin']),
goodCartNum() { //计算单个饮品添加到购物车的数量
return (id) => this.cart.reduce((acc, cur) => { // (acc) (累加器) (cur) (当前元素的值)
if (cur.id === id) { //通过id到购物车中找到此商品
return acc += cur.number //计算
}
return acc
}, 0)
},
getCartGoodsNumber() { //计算购物车总数
return this.cart.reduce((acc, cur) => acc + cur.number, 0)
},
getCartGoodsPrice() { //计算购物车总价
return this.cart.reduce((acc, cur) => acc + cur.number * cur.price, 0)
},
disabledPay() { //是否达到起送价
return this.orderType == 'takeout' && (this.getCartGoodsPrice < this.store.min_price) ? true : false
},
spread() { //差多少元起送
if (this.orderType != 'takeout') return
return parseFloat((this.store.min_price - this.getCartGoodsPrice).toFixed(2))
},
menuCartNum() { //每一栏的数量,进行判断赛选
return (id) => this.cart.reduce((acc, cur) => {
if (cur.cate_id === id) {
return acc += cur.number
}
return acc
}, 0)
},
},
async onLoad() {
await this.init() //在生命周期调用初始化页面
},
methods: {
...mapMutations(['SET_ORDER_TYPE']), //点击自取,设置order_type的参数
...mapActions(['getStore']), //引入getStore方法
async init() { //页面初始化方法
await this.getStore() //调用getStore方法,对store变量进行商家数据填充
this.goods = await this.$api('goods') //对goods变量进行货物数据填充
},
takout() { //点击外卖
if (this.orderType == 'takeout') return //如果是选取外卖状态,则返回
if (!this.isLogin) { //未登录
uni.navigateTo({
url: '/pages/login/login'
})
return
}
uni.navigateTo({ //跳转地址页面
url: '/pages/address/address'
})
},
handleGoodsScroll({
detail
}) { //商品列表滚动事件
if (!this.sizeCalcState) {
this.calcSize() //为每个item添加一个top高度
}
const {
scrollTop
} = detail //detail为参数,可以查看scrollTop,根据scrollTop来联动左侧item
let tabs = this.goods.filter(item => item.top <= scrollTop).reverse() //过滤、排序,
if (tabs.length > 0) {
this.currentCateId = tabs[0].id //定义currentCateId 如果跟item.id一样,则使用:class
}
},
calcSize() {
let h = 10
//该方法位获取轮播图的高度
let view = uni.createSelectorQuery().select('#ads') //获取轮播图节点
view.fields({
size: true
}, data => {
h += Math.floor(data.height) //赋予h参数轮播图高度
}).exec()
//获取每个item的高度,叠加,装入数据里面
this.goods.forEach(item => {
let view = uni.createSelectorQuery().select(`#cate-${item.id}`) //每个商品view
view.fields({
size: true
}, data => {
item.top = h //为每个item设置一个top属性
h += data.height //对h属性增加一个item高度
item.bottom = h //设置item的底部bottom属性
}).exec()
})
this.sizeCalcState = true
},
handleMenuTap(id) { //点击菜单项事件
if (!this.sizeCalcState) {
this.calcSize() //为每个item添加一个top高度
}
this.currentCateId = id
this.$nextTick(() => this.cateScrollTop = this.goods.find(item => item.id == id).top) //
},
showGoodDetailModal(item, good) { //打开商品详情
this.good = JSON.parse(JSON.stringify({
...good,
number: 1
})) //当前饮品赋予good值,多加一个属性number
this.goodDetailModalVisible = true
this.category = JSON.parse(JSON.stringify(item)) //当前饮品所在种类赋值
},
closeGoodDetailModal() { //关闭饮品详情模态框
this.goodDetailModalVisible = false
this.good = {} //清空
this.category = {}
},
getGoodSelectedProps(good, type = 'text') { //计算当前饮品所选属性 文字
if (good.use_property) {
let props = []
good.property.forEach(({
values
}) => {
values.forEach(value => {
if (value.is_default) {
props.push(type === 'text' ? value.value : value.id)
}
})
})
return type === 'text' ? props.join(',') : props
}
return ''
},
changePropertyDefault(index, key) { //改变默认属性值
this.good.property[index].values.forEach(value => this.$set(value, 'is_default', 0)) //遍历每个is_default值为0
this.good.property[index].values[key].is_default = 1 //点击后 改为 1 做状态调整
this.good.number = 1 //点击后该商品数量为1
},
handleAddToCart(cate, good, num) { //添加到购物车 +1
const index = this.cart.findIndex(item => { //findIndex() 方法返回传入一个条件(函数)符合条件的数组第一个元素位置,如果没有符合条件的元素返回 -1
if (good.use_property) { //如果是选规格的
return (item.id === good.id) && (item.props_text === good.props_text)
} else {
return item.id === good.id
}
})
if (index > -1) {
this.cart[index].number += num //如果对象已有,直接在已有的对象上加1
} else {
this.cart.push({ //否则 重新添加进购物车
id: good.id,
cate_id: cate.id,
name: good.name,
price: good.price,
number: num,
image: good.images,
use_property: good.use_property,
props_text: good.props_text,
props: good.props
})
}
},
handleReduceFromCart(item, good) { //减少商品 -1
const index = this.cart.findIndex(item => item.id === good.id)
this.cart[index].number -= 1
if (this.cart[index].number <= 0) {
this.cart.splice(index, 1) //如果减少到了0,则剔除该商品
}
},
handlePropertyAdd() { //加一个商品
this.good.number += 1
},
handlePropertyReduce() { //减一个商品
if (this.good.number === 1) return
this.good.number -= 1
},
handleAddToCartInModal() { //加入购物车
const product = Object.assign({}, this.good, {
props_text: this.getGoodSelectedProps(this.good),
props: this.getGoodSelectedProps(this.good, 'id')
})
this.handleAddToCart(this.category, product, this.good.number)
this.closeGoodDetailModal()
},
}
}
</script>
<style lang="scss" scoped>
@import '~@/pages/menu/menu.scss';
</style>
menu.scss新增.cart-box
.cart-box {
position: absolute;
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
height: 96rpx;
border-radius: 48rpx;
box-shadow: 0 0 20rpx rgba(0, 0, 0, 0.2);
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 9999;
.cart-img {
width: 96rpx;
height: 96rpx;
position: relative;
margin-top: -48rpx;
}
.pay-btn {
height: 100%;
padding: 0 30rpx;
color: #FFFFFF;
border-radius: 0 50rpx 50rpx 0;
display: flex;
align-items: center;
font-size: $font-size-base;
}
.mark {
padding-left: 46rpx;
margin-right: 30rpx;
position: relative;
.tag {
background-color: $color-warning;
color: $text-color-white;
display: flex;
justify-content: center;
align-items: center;
font-size: $font-size-sm;
position: absolute;
right: -10rpx;
top: -50rpx;
border-radius: 100%;
padding: 4rpx;
width: 40rpx;
height: 40rpx;
opacity: .9;
}
}
.price {
flex: 1;
color: $text-color-base;
}
}
5、购物车管理中如图4.2.9为购物车清单汇总列表,点击标红第1处(@tap="openCartPopup"方法)弹出自定义控件popup-layer,其功能可对所有已选商品进行增减、清空。

在components文件夹中引入popup-layer组件完成购物车中商品详情(图4.2.9中标红第2处),并在menu.scss中加入.cart-popup样式。定义一个cartPopupVisible布尔参数判断popup-layer的显示与隐藏,同时利用cart购物车数组循环获取数据,使用每个item中商品名称、价格、个数等信息进行展示,通过handleCartItemReduce(index)、handleCartItemAdd(index)方法进行增减,方法代码如下:
handleCartItemAdd(index) {//购物车栏减少
this.cart[index].number += 1
},
handleCartItemReduce(index) {//购物车栏增加
if(this.cart[index].number === 1) {
this.cart.splice(index, 1)
} else {
this.cart[index].number -= 1
}
if(!this.cart.length) {
this.cartPopupVisible = false
}
}
通过handleCartClear方法与图4.2.9中标红第3处进行交互完成清空购物车中商品,如图4.2.10。通过uniapp框架中uni.showModal()方法完成提示,在success回调函数中进行清空购物车商品,代码如下:

handleCartClear() { //清空购物车
uni.showModal({
title: '提示',
content: '确定清空购物车么',
success: ({confirm}) => {
if(confirm) {
this.cartPopupVisible = false
this.cart = []
}
}
})
}
6、自此,即完成了购物车管理功能研发工作,团队成员应使用SourceTree工具执行版本提交,以创建此工单研发代码的历史版本记录。
本工单完整代码如下:
menu.vue完整代码
<template>
<view class="container">
<view class="main">
<!-- 头部 -->
<view class="nav">
<view class="header">
<!-- 未接单 如果是自取 ,默认自取 显示商家距离 -->
<view class="left" v-if="orderType == 'takein'">
<view class="store-name">
<text>{{ store.name }}</text>
<view class="iconfont iconarrow-right"></view>
</view>
<view class="store-location">
<image src='/static/images/order/location.png' style="width: 30rpx; height: 30rpx;"
class="mr-10"></image>
<text>距离您 {{ store.distance_text}} 米</text>
</view>
</view>
<!-- 如果是外卖 ,显示街道 -->
<view class="left overflow-hidden" v-else>
<view class="d-flex align-items-center overflow-hidden">
<image src='/static/images/order/location.png' style="width: 30rpx; height: 30rpx;"
class="mr-10"></image>
<view class="font-size-extra-lg text-color-base font-weight-bold text-truncate">
{{ address.street }}
</view>
</view>
<view class="font-size-sm text-color-assist overflow-hidden text-truncate">
由<text class="text-color-base" style="margin: 0 10rpx">{{ store.name }}</text>配送
</view>
</view>
<!-- 已接单 点击后样式变化 -->
<view class="right">
<view class="dinein" :class="{active: orderType == 'takein'}" @tap="SET_ORDER_TYPE('takein')">
<text>自取</text>
</view>
<view class="takeout" :class="{active: orderType == 'takeout'}" @tap="takout">
<text>外卖</text>
</view>
</view>
</view>
<view class="coupon">
<text class="title">"霸气mini卡"超级购券活动,赶紧去购买</text>
<view class="iconfont iconarrow-right"></view>
</view>
</view>
<!-- 中间左侧 -->
<view class="content">
<scroll-view class="menus" scroll-with-animation scroll-y>
<view class="wrapper">
<view class="menu" :id="`menu-${item.id}`" :class="{'current': item.id === currentCateId}"
@tap="handleMenuTap(item.id)" v-for="(item, index) in goods" :key="index">
<text>{{ item.name }}</text>
<!-- 加入购物车后 -->
<view class="dot" v-show="menuCartNum(item.id)">{{ menuCartNum(item.id) }}</view>
</view>
</view>
</scroll-view>
<!-- 中间右侧 -->
<scroll-view class="goods" scroll-with-animation scroll-y :scroll-top="cateScrollTop"
@scroll="handleGoodsScroll">
<view class="wrapper">
<!-- 轮播图 -->
<swiper class="ads" id="ads" autoplay :interval="3000" indicator-dots>
<swiper-item v-for="(item, index) in ads" :key='index'>
<image :src="item.image"></image>
</swiper-item>
</swiper>
<!-- 商品列表 -->
<view class="list">
<!-- category begin -->
<view class="category" v-for="(item, index) in goods" :key="index" :id="`cate-${item.id}`">
<!-- 大标题 系列 -->
<view class="title">
<text>{{ item.name }}</text>
<image :src="item.icon" class="icon"></image>
</view>
<view class="items">
<!-- 每一个商品 begin -->
<view class="good" v-for="(good, key) in item.goods_list" :key="key">
<image :src="good.images" class="image" @tap="showGoodDetailModal(item, good)">
</image>
<view class="right">
<text class="name">{{ good.name }}</text>
<text class="tips">{{ good.content }}</text>
<view class="price_and_action">
<text class="price">¥{{ good.price }}</text>
<!-- 选规格 use_property 1 -->
<view class="btn-group" v-if="good.use_property">
<button type="primary" class="btn property_btn" hover-class="none"
size="mini" @tap="showGoodDetailModal(item, good)">
选规格
</button>
<view class="dot" v-show="goodCartNum(good.id)">
{{ goodCartNum(good.id) }}</view>
</view>
<!-- 无选规格 use_property 0 正常增减数量-->
<view class="btn-group" v-else>
<!-- 减少 -->
<button type="default" plain class="btn reduce_btn" size="mini"
hover-class="none" v-show="goodCartNum(good.id)"
@tap="handleReduceFromCart(item, good)">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number" v-show="goodCartNum(good.id)">
{{ goodCartNum(good.id) }}
</view>
<!-- 增加 -->
<button type="primary" class="btn add_btn" size="min"
hover-class="none" @tap="handleAddToCart(item, good, 1)">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
</view>
</view>
<!-- 商品 end -->
</view>
</view>
<!-- category end -->
</view>
</view>
</scroll-view>
</view>
<!-- 购物车栏 begin -->
<view class="cart-box" v-if="cart.length > 0">
<view class="mark">
<image src="/static/images/menu/cart.png" class="cart-img" @tap="openCartPopup"></image>
<view class="tag">{{ getCartGoodsNumber }}</view>
</view>
<view class="price">¥{{ getCartGoodsPrice }}</view>
<button type="primary" class="pay-btn" @tap="toPay" :disabled="disabledPay">
{{ disabledPay ? `差${spread}元起送` : '去结算' }}
</button>
</view>
<!-- 购物车栏 end -->
</view>
<!-- 商品详情模态框 begin -->
<modal :show="goodDetailModalVisible" class="good-detail-modal" color="#5A5B5C" width="90%" custom
padding="0rpx" radius="12rpx">
<!-- 上方介绍 -->
<view class="cover">
<image v-if="good.images" :src="good.images" class="image"></image>
<view class="btn-group">
<image src="/static/images/menu/share-good.png"></image>
<image src="/static/images/menu/close.png" @tap="closeGoodDetailModal"></image>
</view>
</view>
<scroll-view class="detail" scroll-y>
<view class="wrapper">
<!-- 商品名称 -->
<view class="basic">
<view class="name">{{ good.name }}</view>
<view class="tips">{{ good.content }}</view>
</view>
<!-- 选规格 种类-->
<view class="properties" v-if="good.use_property">
<view class="property" v-for="(item, index) in good.property" :key="index">
<!-- 标题 -->
<view class="title">
<text class="name">{{ item.name }}</text>
<view class="desc" v-if="item.desc">({{ item.desc }})</view>
</view>
<!--选规格 内容 -->
<view class="values">
<view class="value" v-for="(value, key) in item.values" :key="key"
:class="{'default': value.is_default}" @tap="changePropertyDefault(index, key)">
{{ value.value }}
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="action">
<!-- 左侧金额 -->
<view class="left">
<view class="price">¥{{ good.price }}</view>
<!-- 选规格的详细信息 -->
<view class="props" v-if="getGoodSelectedProps(good)">
{{ getGoodSelectedProps(good) }}
</view>
</view>
<!-- 右侧 选择数量 -->
<view class="btn-group">
<button type="default" plain class="btn" size="mini" hover-class="none" @tap="handlePropertyReduce">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number">{{ good.number }}</view>
<button type="primary" class="btn" size="min" hover-class="none" @tap="handlePropertyAdd">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
<!-- 下方 加入购物车 按钮 -->
<view class="add-to-cart-btn" @tap="handleAddToCartInModal">
<view>加入购物车</view>
</view>
</modal>
<!-- 商品详情模态框 end -->
<!-- 购物车详情popup begin-->
<popup-layer direction="top" :show-pop="cartPopupVisible" class="cart-popup">
<template slot="content">
<view class="top">
<text @tap="handleCartClear">清空</text>
</view>
<scroll-view class="cart-list" scroll-y>
<view class="wrapper">
<!-- 购物车列表 -->
<view class="item" v-for="(item, index) in cart" :key="index">
<view class="left">
<view class="name">{{ item.name }}</view>
<view class="props">{{ item.props_text }}</view>
</view>
<view class="center">
<text>¥{{ item.price }}</text>
</view>
<view class="right">
<button type="default" plain size="mini" class="btn" hover-class="none"
@tap="handleCartItemReduce(index)">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number">{{ item.number }}</view>
<button type="primary" class="btn" size="min" hover-class="none"
@tap="handleCartItemAdd(index)">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
<!-- 外卖 包装费 -->
<view class="item" v-if="orderType == 'takeout' && store.packing_fee">
<view class="left">
<view class="name">包装费</view>
</view>
<view class="center">
<text>¥{{ parseFloat(store.packing_fee) }}</text>
</view>
<view class="right invisible">
<button type="default" plain size="mini" class="btn" hover-class="none">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number">1</view>
<button type="primary" class="btn" size="min" hover-class="none">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
</view>
</scroll-view>
</template>
</popup-layer>
<!-- 购物车详情popup end-->
</view>
</template>
<script>
import {
mapState,
mapActions,
mapGetters,
mapMutations
} from 'vuex'
import modal from '@/components/modal/modal'
import popupLayer from '@/components/popup-layer/popup-layer'
export default {
components: {
modal
},
data() {
return {
goods: [], //所有商品
currentCateId: 6905, //默认分类
cateScrollTop: 0, //scroll-view滑动刻度
goodDetailModalVisible: false, //是否饮品详情模态框
good: {}, //当前饮品
cart: [], //购物车
category: {}, //当前饮品所在分类
cartPopupVisible: false,//购物栏popup是否显示
ads: [{image: 'https://s3.uuu.ovh/imgs/2025/04/06/66a62739b6bb6c6e.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/5202f8ff24613d35.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/96228fabd6c0610c.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/a69f605a1dcc3496.png'},
{image: 'https://s3.uuu.ovh/imgs/2025/04/06/9a0632bfa5685857.png'}
], //轮播图数据
}
},
computed: {
...mapState(['orderType', 'store', 'address']), //引用数据
...mapGetters(['isLogin']),
goodCartNum() { //计算单个饮品添加到购物车的数量
return (id) => this.cart.reduce((acc, cur) => { // (acc) (累加器) (cur) (当前元素的值)
if (cur.id === id) { //通过id到购物车中找到此商品
return acc += cur.number //计算
}
return acc
}, 0)
},
getCartGoodsNumber() { //计算购物车总数
return this.cart.reduce((acc, cur) => acc + cur.number, 0)
},
getCartGoodsPrice() { //计算购物车总价
return this.cart.reduce((acc, cur) => acc + cur.number * cur.price, 0)
},
disabledPay() { //是否达到起送价
return this.orderType == 'takeout' && (this.getCartGoodsPrice < this.store.min_price) ? true : false
},
spread() { //差多少元起送
if (this.orderType != 'takeout') return
return parseFloat((this.store.min_price - this.getCartGoodsPrice).toFixed(2))
},
menuCartNum() { //每一栏的数量,进行判断赛选
return (id) => this.cart.reduce((acc, cur) => {
if (cur.cate_id === id) {
return acc += cur.number
}
return acc
}, 0)
},
},
async onLoad() {
await this.init() //在生命周期调用初始化页面
},
methods: {
...mapMutations(['SET_ORDER_TYPE']), //点击自取,设置order_type的参数
...mapActions(['getStore']), //引入getStore方法
async init() { //页面初始化方法
await this.getStore() //调用getStore方法,对store变量进行商家数据填充
this.goods = await this.$api('goods') //对goods变量进行货物数据填充
},
takout() { //点击外卖
if (this.orderType == 'takeout') return //如果是选取外卖状态,则返回
if (!this.isLogin) { //未登录
uni.navigateTo({
url: '/pages/login/login'
})
return
}
uni.navigateTo({ //跳转地址页面
url: '/pages/address/address'
})
},
handleGoodsScroll({
detail
}) { //商品列表滚动事件
if (!this.sizeCalcState) {
this.calcSize() //为每个item添加一个top高度
}
const {
scrollTop
} = detail //detail为参数,可以查看scrollTop,根据scrollTop来联动左侧item
let tabs = this.goods.filter(item => item.top <= scrollTop).reverse() //过滤、排序,
if (tabs.length > 0) {
this.currentCateId = tabs[0].id //定义currentCateId 如果跟item.id一样,则使用:class
}
},
calcSize() {
let h = 10
//该方法位获取轮播图的高度
let view = uni.createSelectorQuery().select('#ads') //获取轮播图节点
view.fields({
size: true
}, data => {
h += Math.floor(data.height) //赋予h参数轮播图高度
}).exec()
//获取每个item的高度,叠加,装入数据里面
this.goods.forEach(item => {
let view = uni.createSelectorQuery().select(`#cate-${item.id}`) //每个商品view
view.fields({
size: true
}, data => {
item.top = h //为每个item设置一个top属性
h += data.height //对h属性增加一个item高度
item.bottom = h //设置item的底部bottom属性
}).exec()
})
this.sizeCalcState = true
},
handleMenuTap(id) { //点击菜单项事件
if (!this.sizeCalcState) {
this.calcSize() //为每个item添加一个top高度
}
this.currentCateId = id
this.$nextTick(() => this.cateScrollTop = this.goods.find(item => item.id == id).top) //
},
showGoodDetailModal(item, good) { //打开商品详情
this.good = JSON.parse(JSON.stringify({
...good,
number: 1
})) //当前饮品赋予good值,多加一个属性number
this.goodDetailModalVisible = true
this.category = JSON.parse(JSON.stringify(item)) //当前饮品所在种类赋值
},
closeGoodDetailModal() { //关闭饮品详情模态框
this.goodDetailModalVisible = false
this.good = {} //清空
this.category = {}
},
getGoodSelectedProps(good, type = 'text') { //计算当前饮品所选属性 文字
if (good.use_property) {
let props = []
good.property.forEach(({
values
}) => {
values.forEach(value => {
if (value.is_default) {
props.push(type === 'text' ? value.value : value.id)
}
})
})
return type === 'text' ? props.join(',') : props
}
return ''
},
changePropertyDefault(index, key) { //改变默认属性值
this.good.property[index].values.forEach(value => this.$set(value, 'is_default', 0)) //遍历每个is_default值为0
this.good.property[index].values[key].is_default = 1 //点击后 改为 1 做状态调整
this.good.number = 1 //点击后该商品数量为1
},
handleAddToCart(cate, good, num) { //添加到购物车 +1
const index = this.cart.findIndex(item => { //findIndex() 方法返回传入一个条件(函数)符合条件的数组第一个元素位置,如果没有符合条件的元素返回 -1
if (good.use_property) { //如果是选规格的
return (item.id === good.id) && (item.props_text === good.props_text)
} else {
return item.id === good.id
}
})
if (index > -1) {
this.cart[index].number += num //如果对象已有,直接在已有的对象上加1
} else {
this.cart.push({ //否则 重新添加进购物车
id: good.id,
cate_id: cate.id,
name: good.name,
price: good.price,
number: num,
image: good.images,
use_property: good.use_property,
props_text: good.props_text,
props: good.props
})
}
},
handleReduceFromCart(item, good) { //减少商品 -1
const index = this.cart.findIndex(item => item.id === good.id)
this.cart[index].number -= 1
if (this.cart[index].number <= 0) {
this.cart.splice(index, 1) //如果减少到了0,则剔除该商品
}
},
handlePropertyAdd() { //加一个商品
this.good.number += 1
},
handlePropertyReduce() { //减一个商品
if (this.good.number === 1) return
this.good.number -= 1
},
handleAddToCartInModal() { //加入购物车
const product = Object.assign({}, this.good, {
props_text: this.getGoodSelectedProps(this.good),
props: this.getGoodSelectedProps(this.good, 'id')
})
this.handleAddToCart(this.category, product, this.good.number)
this.closeGoodDetailModal()
},
handleCartItemAdd(index) {//购物车栏减少
this.cart[index].number += 1
},
handleCartItemReduce(index) {//购物车栏增加
if(this.cart[index].number === 1) {
this.cart.splice(index, 1)
} else {
this.cart[index].number -= 1
}
if(!this.cart.length) {
this.cartPopupVisible = false
}
},
openCartPopup() { //打开/关闭购物车列表popup
this.cartPopupVisible = !this.cartPopupVisible
},
handleCartClear() { //清空购物车
uni.showModal({
title: '提示',
content: '确定清空购物车么',
success: ({confirm}) => {
if(confirm) {
this.cartPopupVisible = false
this.cart = []
}
}
})
},
}
}
</script>
<style lang="scss" scoped>
@import '~@/pages/menu/menu.scss';
</style>
menu.scss 完整代码
/* 头部 */
/* #ifdef H5 */
page {
min-height: 100%;
}
/* #endif */
.container {
overflow: hidden;
position: relative;
}
.main {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
}
.nav {
width: 100%;
height: 212rpx;
flex-shrink: 0;
display: flex;
flex-direction: column;
.header {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background-color: #ffffff;
height: 140rpx;
.left {
flex: 1;
display: flex;
flex-direction: column;
.store-name {
display: flex;
justify-content: flex-start;
align-items: center;
font-size: $font-size-lg;
margin-bottom: 10rpx;
.iconfont {
margin-left: 10rpx;
line-height: 100%;
}
}
.store-location {
display: flex;
justify-content: flex-start;
align-items: center;
color: $text-color-assist;
font-size: $font-size-sm;
.iconfont {
vertical-align: middle;
display: table-cell;
color: $color-primary;
line-height: 100%;
}
}
}
.right {
background-color: $bg-color-grey;
border-radius: 38rpx;
display: flex;
align-items: center;
font-size: $font-size-sm;
padding: 0 38rpx;
color: $text-color-assist;
.dinein, .takeout {
position: relative;
display: flex;
align-items: center;
&.active {
padding: 14rpx 38rpx;
color: #ffffff;
background-color: $color-primary;
border-radius: 38rpx;
}
}
.takeout {
margin-left: 20rpx;
height: 100%;
flex: 1;
padding: 14rpx 0;
}
.dinein.active {
margin-left: -38rpx;
}
.takeout.active {
margin-right: -38rpx;
}
}
}
.coupon {
flex: 1;
width: 100%;
background-color: $bg-color-primary;
font-size: $font-size-base;
color: $color-primary;
padding: 0 20rpx;
display: flex;
align-items: center;
overflow: hidden;
.title {
flex: 1;
margin-left: 10rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.iconfont {
line-height: 100%;
}
}
}
.content {
flex: 1;
overflow: hidden;
width: 100%;
display: flex;
/* 下方left */
.menus {
width: 200rpx;
height: 100%;
overflow: hidden;
background-color: $bg-color-grey;
.wrapper {
width: 100%;
height: 100%;
.menu {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 30rpx 20rpx;
font-size: 26rpx;
color: $text-color-assist;
position: relative;
&:nth-last-child(1) {
margin-bottom: 130rpx;
}
&.current {
background-color: #ffffff;
color: $text-color-base;
}
.dot {
position: absolute;
width: 34rpx;
height: 34rpx;
line-height: 34rpx;
font-size: 22rpx;
background-color: $color-primary;
color: #ffffff;
top: 16rpx;
right: 10rpx;
border-radius: 100%;
text-align: center;
}
}
}
}
/* 下方right */
.goods {
flex: 1;
height: 100%;
overflow: hidden;
background-color: #ffffff;
.wrapper {
width: 100%;
height: 100%;
padding: 20rpx;
/* 轮播图 */
.ads {
height: calc(300 / 550 * 510rpx);
image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
}
/* 商品列表 */
.list {
width: 100%;
font-size: $font-size-base;
padding-bottom: 130rpx;
.category {
width: 100%;
.title {
padding: 30rpx 0;
display: flex;
align-items: center;
color: $text-color-base;
.icon {
width: 38rpx;
height: 38rpx;
margin-left: 10rpx;
}
}
}
.items {
display: flex;
flex-direction: column;
padding-bottom: -30rpx;
.good {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.image {
width: 160rpx;
height: 160rpx;
margin-right: 20rpx;
border-radius: 8rpx;
}
.right {
flex: 1;
height: 160rpx;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
padding-right: 14rpx;
.name {
font-size: $font-size-base;
margin-bottom: 10rpx;
}
.tips {
width: 100%;
height: 40rpx;
line-height: 40rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: $font-size-sm;
color: $text-color-assist;
margin-bottom: 10rpx;
}
.price_and_action {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.price {
font-size: $font-size-base;
font-weight: 600;
}
.btn-group {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
.btn {
padding: 0 20rpx;
box-sizing: border-box;
font-size: $font-size-sm;
height: 44rpx;
line-height: 44rpx;
&.property_btn {
border-radius: 24rpx;
}
&.add_btn,
&.reduce_btn {
padding: 0;
width: 44rpx;
border-radius: 44rpx;
}
}
.dot {
position: absolute;
background-color: #ffffff;
border: 1px solid $color-primary;
color: $color-primary;
font-size: $font-size-sm;
width: 36rpx;
height: 36rpx;
line-height: 36rpx;
text-align: center;
border-radius: 100%;
right: -12rpx;
top: -10rpx;
}
.number {
width: 44rpx;
height: 44rpx;
line-height: 44rpx;
text-align: center;
}
}
}
}
}
}
}
}
}
}
.modal-box {
max-height: 90vh;
}
.good-detail-modal {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.cover {
height: 320rpx;
padding: 30rpx 0;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.image {
width: 260rpx;
height: 260rpx;
}
.btn-group {
position: absolute;
right: 10rpx;
top: 30rpx;
display: flex;
align-items: center;
justify-content: space-around;
image {
width: 80rpx;
height: 80rpx;
}
}
}
.detail {
width: 100%;
min-height: 1vh;
max-height: calc(90vh - 320rpx - 80rpx - 120rpx);
.wrapper {
width: 100%;
height: 100%;
overflow: hidden;
.basic {
padding: 0 20rpx 30rpx;
display: flex;
flex-direction: column;
.name {
font-size: $font-size-base;
color: $text-color-base;
margin-bottom: 10rpx;
}
.tips {
font-size: $font-size-sm;
color: $text-color-grey;
}
}
.properties {
width: 100%;
border-top: 2rpx solid $bg-color-grey;
padding: 10rpx 30rpx 0;
display: flex;
flex-direction: column;
.property {
width: 100%;
display: flex;
flex-direction: column;
margin-bottom: 30rpx;
padding-bottom: -16rpx;
.title {
width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 20rpx;
.name {
font-size: 26rpx;
color: $text-color-base;
margin-right: 20rpx;
}
.desc {
flex: 1;
font-size: $font-size-sm;
color: $color-primary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.values {
width: 100%;
display: flex;
flex-wrap: wrap;
.value {
border-radius: 8rpx;
background-color: $bg-color-grey;
padding: 16rpx 30rpx;
font-size: 26rpx;
color: $text-color-assist;
margin-right: 16rpx;
margin-bottom: 16rpx;
&.default {
background-color: $color-primary;
color: $text-color-white;
}
}
}
}
}
}
}
.action {
display: flex;
align-items: center;
justify-content: space-between;
background-color: $bg-color-grey;
height: 120rpx;
padding: 0 26rpx;
.left {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 20rpx;
overflow: hidden;
.price {
font-size: $font-size-lg;
color: $text-color-base;
}
.props {
color: $text-color-assist;
font-size: 24rpx;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.btn-group {
display: flex;
align-items: center;
justify-content: space-around;
.number {
font-size: $font-size-base;
width: 44rpx;
height: 44rpx;
line-height: 44rpx;
text-align: center;
}
.btn {
padding: 0;
font-size: $font-size-base;
width: 44rpx;
height: 44rpx;
line-height: 44rpx;
border-radius: 100%;
}
}
}
.add-to-cart-btn {
display: flex;
justify-content: center;
align-items: center;
background-color: $color-primary;
color: $text-color-white;
font-size: $font-size-base;
height: 80rpx;
border-radius: 0 0 12rpx 12rpx;
}
}
.cart-box {
position: absolute;
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
height: 96rpx;
border-radius: 48rpx;
box-shadow: 0 0 20rpx rgba(0, 0, 0, 0.2);
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 9999;
.cart-img {
width: 96rpx;
height: 96rpx;
position: relative;
margin-top: -48rpx;
}
.pay-btn {
height: 100%;
padding: 0 30rpx;
color: #FFFFFF;
border-radius: 0 50rpx 50rpx 0;
display: flex;
align-items: center;
font-size: $font-size-base;
}
.mark {
padding-left: 46rpx;
margin-right: 30rpx;
position: relative;
.tag {
background-color: $color-warning;
color: $text-color-white;
display: flex;
justify-content: center;
align-items: center;
font-size: $font-size-sm;
position: absolute;
right: -10rpx;
top: -50rpx;
border-radius: 100%;
padding: 4rpx;
width: 40rpx;
height: 40rpx;
opacity: .9;
}
}
.price {
flex: 1;
color: $text-color-base;
}
}
.cart-popup {
.top {
background-color: $bg-color-primary;
color: $color-primary;
padding: 10rpx 30rpx;
font-size: 24rpx;
text-align: right;
}
.cart-list {
background-color: #FFFFFF;
width: 100%;
overflow: hidden;
min-height: 1vh;
max-height: 60vh;
.wrapper {
height: 100%;
display: flex;
flex-direction: column;
padding: 0 30rpx;
margin-bottom: 156rpx;
.item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
position: relative;
&::after {
content: ' ';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: $border-color;
height: 2rpx;
transform: scaleY(.6);
}
.left {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
margin-right: 30rpx;
.name {
font-size: $font-size-sm;
color: $text-color-base;
}
.props {
color: $text-color-assist;
font-size: 24rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.center {
margin-right: 120rpx;
font-size: $font-size-base;
}
.right {
display: flex;
align-items: center;
justify-content: space-between;
.btn {
width: 46rpx;
height: 46rpx;
border-radius: 100%;
padding: 0;
text-align: center;
line-height: 46rpx;
}
.number {
font-size: $font-size-base;
width: 46rpx;
height: 46rpx;
text-align: center;
line-height: 46rpx;
}
}
}
}
}
}