任务4-1操作步骤:点餐界面业务逻辑

任务四 工单1

本工单最终效果图:


4.1.1 左右联动效果图.png
4.1.2 模态框效果图.png

1、鉴于团队成员在各自的分支上独立开展工作,一旦团队成员A完成了其开发任务,便已将代码变更推送至GitCode远程仓库。现在,小组成员B开始进行本工单任务的研发工作,需要从远程仓库获取代码,并将其合并到小组成员B的本地项目文件中。
通过SourceTree同步远程代码变更,点击图4.1.3所示的“获取”区域,在图4.1.4中,观察到YunLiProgram项目“拉取”区域显示有3项更新。这3项更新由团队成员A提交的上一版本代码构成,点击“拉取”即可将成员A的代码与团队成员B的项目代码合并。


4.1.3 SourceTree远程获取.png
4.1.4 拉取合并到本地项目.png
点击后,将弹出如图4.1.5所示的提示框,其中包含需要拉取的远程分支信息、本地目标分支以及是否合并到本地的选项。点击“拉取”按钮后,所有远程代码将合并到成员B的本地代码库中,此时master、origin/master、origin/HEAD分支均更新至最新状态,如图4.1.6所示。
4.1.5 确认拉取.png
4.1.6 已更新合并项目到本地.png

2、成员B在更新代码后,着手开发“点餐配送”界面的交互效果。在顾客挑选商品之前,必须明确订单是“自取”还是“外卖”配送类型。利用mapState中的orderType属性进行控制,其中“自取”选项对应值为takein,而“外卖”选项对应值为takeout。

点击“自取”,左侧将展示距离最近的云鲤奶茶店的店名及与顾客的距离;点击“外卖”,系统首先通过isLogin功能判断用户是否已登录。若用户已登录,则跳转至选择配送地址的界面,选定地址后返回至“点餐”界面,此时左侧将显示顾客的地址和云鲤奶茶店的店名,具体效果见图4.1.7。
4.1.7 “自取”与“外卖”效果.png

商店数据和顾客地址通过mapState中的store和address属性获取。其中,store.name属性代表店名,store.distance_text属性表示距离,而address.street属性则对应顾客选择的地址。关键代码片段如下:

    <!-- 未接单    如果是自取 ,默认自取 显示商家距离  -->
    <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>

点击“自取”按钮,通过调用@tap="SET_ORDER_TYPE('takein')"方法进行交互,利用store中的SET_ORDER_TYPE()方法将orderType设置为takein;点击“外卖”按钮,则使用@tap="takout"方法进行交互。

    <!-- 已接单  点击后样式变化 -->
    <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>

以上完整代码:

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}"
                            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>
                    <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" ></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" >
                                                        选规格
                                                    </button>
                                                </view>
                                                <!-- 无选规格 use_property 0  正常增减数量-->
                                                <view class="btn-group" v-else>
                                                    <button type="default"  plain class="btn reduce_btn" 
                                                    size="mini" hover-class="none" >
                                                        <view class="iconfont iconsami-select"></view>
                                                    </button>
                                                    <view class="number" >1</view>
                                                    <button type="primary" class="btn add_btn" 
                                                    size="min" hover-class="none" >
                                                        <view class="iconfont iconadd-select"></view>
                                                    </button>
                                                </view>
                                            </view>
                                        </view>
                                    </view>
                                    <!-- 商品 end -->
                                </view>
                            </view>
                            <!-- category end -->
                        </view>
                    </view>
                </scroll-view>
            </view>
        </view>
    </view>
</template>

<script>
    import {mapState,mapActions,mapGetters,mapMutations} from 'vuex'
    export default {
        data() {
            return {
                goods: [], //所有商品
                currentCateId: 6905, //默认分类
                ads: [{image: 'https://s3.uuu.ovh/imgs/2024/12/26/93968f66a1838191.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/1aa2d8b3ff40315a.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/35ea26b9f691d977.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/a391d87d1661cbc2.jpg'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/6fbf2d2a5e8be9d0.jpg'}
                ], //轮播图数据
            }
        },
        computed: {
            ...mapState(['orderType','store','address']),//引用数据
            ...mapGetters(['isLogin']),
        },
        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'
                })
            },
        }
    }
</script>
<style lang="scss" scoped>
    @import '~@/pages/menu/menu.scss';
</style>

3、在完成上述逻辑处理后,接下来对左右两侧的列表进行交互操作,其效果展示在图4.1.8中。当右侧列表向上滑动时,左侧的菜单栏应相应地改变其样式状态以实现联动。

4.1.8 列表模拟滑动效果.png

逻辑相对而言是简洁明了的,右侧的列表展示了商品项(item),而上方则配置了一个轮播图。首先,使用uni.createSelectorQuery().select('#ads')来计算轮播图的高度。接着,通过uni.createSelectorQuery().select(#cate-${item.id})来计算每个商品项的高度。对于goods中的每个对象,设置一个top和bottom值,这个过程通过一个名为calcSize()的方法来完成,具体可参见图4.1.9。

    //该方法位获取轮播图的高度
    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()
4.1.9 item属性(top、bottom).png

通过scroll-view的@scroll="handleGoodsScroll"属性,可以获取当前scroll-view的高度scrollTop。接着,通过比较scrollTop与每个item的高度,可以判断出当前处于哪个菜单种类。最终,将获取到的tabs的id赋值给currentCateId,以此来改变菜单栏的状态。

    handleGoodsScroll({detail}) {   //商品列表滚动事件
        if(!this.sizeCalcState) { //判断是否计算过item高度
            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){  //有tabs数据则取id
            this.currentCateId = tabs[0].id//定义currentCateId 如果跟item.id一样,则使用:class
        }
    }

在完成右侧商品列表的滑动操作后,需要处理点击左侧菜单栏时联动右侧商品位置的逻辑。为左侧的每个菜单项设置点击事件处理器,使用表达式@tap="handleMenuTap(item.id)"来传递item.id参数。定义一个变量cateScrollTop,用于存储scroll-view的滑动位置。当点击事件发生时,利用id找到左侧当前激活的菜单项,并使用this.goods.find(item => item.id == id).top)定位右侧商品列表中对应项的top位置,然后将这个值赋给cateScrollTop。最后,scroll-view将滚动到cateScrollTop指定的位置,实现如图4.1.10所示的效果。具体的代码实现如下:

    handleMenuTap(id) { //点击菜单项事件
        if(!this.sizeCalcState) {
            this.calcSize()//为每个item添加一个top高度
        }
        this.currentCateId = id
        this.$nextTick(() => this.cateScrollTop = this.goods.find(item => item.id == id).top)//找到点击左侧当前菜单列表对应项关联的第一个item的top
    }
4.1.10 点击菜单栏item自动滑动.png

以上完整代码:

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"></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">
                                                        选规格
                                                    </button>
                                                </view>
                                                <!-- 无选规格 use_property 0  正常增减数量-->
                                                <view class="btn-group" v-else>
                                                    <button type="default" plain class="btn reduce_btn" size="mini"
                                                        hover-class="none">
                                                        <view class="iconfont iconsami-select"></view>
                                                    </button>
                                                    <view class="number">1</view>
                                                    <button type="primary" class="btn add_btn" size="min"
                                                        hover-class="none">
                                                        <view class="iconfont iconadd-select"></view>
                                                    </button>
                                                </view>
                                            </view>
                                        </view>
                                    </view>
                                    <!-- 商品 end -->
                                </view>
                            </view>
                            <!-- category end -->
                        </view>
                    </view>
                </scroll-view>
            </view>
        </view>
    </view>
</template>

<script>
    import {
        mapState,
        mapActions,
        mapGetters,
        mapMutations
    } from 'vuex'
    export default {
        data() {
            return {
                goods: [], //所有商品
                currentCateId: 6905, //默认分类
                cateScrollTop: 0,   //scroll-view滑动刻度
                ads: [{image: 'https://s3.uuu.ovh/imgs/2024/12/26/93968f66a1838191.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/1aa2d8b3ff40315a.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/35ea26b9f691d977.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/a391d87d1661cbc2.jpg'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/6fbf2d2a5e8be9d0.jpg'}
                ], //轮播图数据
            }
        },
        computed: {
            ...mapState(['orderType', 'store', 'address']), //引用数据
            ...mapGetters(['isLogin']),
        },
        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) //
            },
        }
    }
</script>
<style lang="scss" scoped>
    @import '~@/pages/menu/menu.scss';
</style>

4、在两个滑动列表的交互操作完成后,接下来需要对商品详情进行详细介绍。当用户点击商品图片时,将弹出一个商品详情的模态框,具体展示如图4.1.11和4.1.12所示。


4.1.11 商品详情模态框样式.png

4.1.12 商品详情模态框样式(选规格).png

首先,引入import modal from '@/components/modal/modal',并在components中注册modal组件。然后,在代码的4.1.13位置设置@tap="showGoodDetailModal(item, good)",以便在点击时传递item和good参数。根据商品good中的use_property属性进行判断,如果use_property的值为0,则弹出如图4.1.11所示的模态框;如果值为1,则弹出如图4.1.12所示的模态框,用于选择规格。


4.1.13 点击后弹出模态框.png

将goodDetailModalVisible设置为控制商品详情模态框的显示状态,通过good中的参数来完成模态框的布局设计,具体参数对应请参见图4.1.14和表1。
4.1.14 模态框参数.png


在模态框中,需特别注意good.property的类型为数组,它代表了商品选定规格的具体内容。通过遍历good.property数组,使用v-for循环,可以获取到每个item的name和values,并据此进行展示。对于某些商品,可能需要添加特定配料,如芝士或酸奶;其他商品可能涉及温度状态或糖度等规格。选定规格后,参考图示下方第7处,这里展示了计算方法(代码如下所示)。其中,is_default标识了默认的规格选项。通过特定方法,将数组元素拼接成文字参数,最终返回并展示给用户。

    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 ''
    }

下面的函数用于修改选择规格属性。它接受index和key作为参数,通过遍历当前规格的values值,并使用set方法来改变is_default的值。默认情况下,is_default的值设为0,当点击后,将其调整为1以区分选择状态,从而实现选择效果。

    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
    } 

4.1.11 效果图的实现依赖于v-if指令来判断是否处于选规格状态,并在menu.scss文件中进一步完善了.good-detail-modal和.modal-box的样式。想要深入了解具体的流程和源代码,可以通过扫描下方的二维码进行学习。
5、在完成本任务工单的研发工作后,团队成员应使用SourceTree工具执行版本提交,如图4.1.15所示,以创建研发代码的历史版本记录。


4.1.15 版本提交记录.png

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">
                                                        <view class="iconfont iconsami-select"></view>
                                                    </button>
                                                    <view class="number">1</view>
                                                    <button type="primary" class="btn add_btn" size="min"
                                                        hover-class="none">
                                                        <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: {}, //当前饮品
                ads: [{image: 'https://s3.uuu.ovh/imgs/2024/12/26/93968f66a1838191.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/1aa2d8b3ff40315a.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/35ea26b9f691d977.png'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/a391d87d1661cbc2.jpg'},
                    {image: 'https://s3.uuu.ovh/imgs/2024/12/26/6fbf2d2a5e8be9d0.jpg'}
                ], //轮播图数据
            }
        },
        computed: {
            ...mapState(['orderType', 'store', 'address']), //引用数据
            ...mapGetters(['isLogin']),
        },
        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
            },
        }
    }
</script>
<style lang="scss" scoped>
    @import '~@/pages/menu/menu.scss';
</style>

menu.scss 新增样式代码:


.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;
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容