任务4-2操作步骤:购物车管理功能

任务四 工单2

最终效果如图:

4.2.1 选择商品、购物车效果图.png
4.2.2 购物车列表效果图.png

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


4.2.3 商品增减处.png

其中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。

4.2.4 商品逻辑完成.png

    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 -->
4.2.5 购物车栏效果图.png

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。


4.2.6 模态框中功能.png

模态框中通过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.2.7 选规格个数展示.png
4.2.8 菜单栏种类个数.png

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,其功能可对所有已选商品进行增减、清空。


4.2.9 购物车详情popup.png

在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回调函数中进行清空购物车商品,代码如下:


4.2.10 清空购物车.png
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;
                    }
                }
            }
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容