任务四 工单1
本工单最终效果图:
1、鉴于团队成员在各自的分支上独立开展工作,一旦团队成员A完成了其开发任务,便已将代码变更推送至GitCode远程仓库。现在,小组成员B开始进行本工单任务的研发工作,需要从远程仓库获取代码,并将其合并到小组成员B的本地项目文件中。
通过SourceTree同步远程代码变更,点击图4.1.3所示的“获取”区域,在图4.1.4中,观察到YunLiProgram项目“拉取”区域显示有3项更新。这3项更新由团队成员A提交的上一版本代码构成,点击“拉取”即可将成员A的代码与团队成员B的项目代码合并。
2、成员B在更新代码后,着手开发“点餐配送”界面的交互效果。在顾客挑选商品之前,必须明确订单是“自取”还是“外卖”配送类型。利用mapState中的orderType属性进行控制,其中“自取”选项对应值为takein,而“外卖”选项对应值为takeout。
点击“自取”,左侧将展示距离最近的云鲤奶茶店的店名及与顾客的距离;点击“外卖”,系统首先通过isLogin功能判断用户是否已登录。若用户已登录,则跳转至选择配送地址的界面,选定地址后返回至“点餐”界面,此时左侧将显示顾客的地址和云鲤奶茶店的店名,具体效果见图4.1.7。商店数据和顾客地址通过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中。当右侧列表向上滑动时,左侧的菜单栏应相应地改变其样式状态以实现联动。
逻辑相对而言是简洁明了的,右侧的列表展示了商品项(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()
通过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
}
以上完整代码:
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所示。
首先,引入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所示的模态框,用于选择规格。
将goodDetailModalVisible设置为控制商品详情模态框的显示状态,通过good中的参数来完成模态框的布局设计,具体参数对应请参见图4.1.14和表1。
在模态框中,需特别注意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所示,以创建研发代码的历史版本记录。
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;
}
}