基于uniapp vue3.0 uView 做一个点单页面(包括加入购物车动画和左右联动)

本文主要是介绍基于uniapp vue3.0 uView 做一个点单页面(包括加入购物车动画和左右联动),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1、实现效果:

下拉有自定义组件(商品卡片、进步器、侧边栏等)源码

2、左右联动功能

使用scroll-view来做右边的菜单页,title的id动态绑定充当锚点

<scroll-view :scroll-into-view="toView" scroll-with-animation="true" class="main" @scroll="scroll" scroll-y><view class="scroll_main"><view class="" v-for="(item,index) in list" :id="'type' + index"><view :id="'title' + index"><u-divider>{{item.meal_name}}</u-divider></view><card v-for="(item2,indax) in item.goods" :data="item2" @change="cardChange"></card></view></view></scroll-view>

侧边栏组件点击事件,返回分类信息,根据分类的id,定位到scroll-view对应的title

<view class="nav"><left-nav :data="list" :current="current" @change="navChange"></left-nav></view>
function navChange(e) {current.value = egetRightScrollDistance()}

 scroll-view属性@scroll用于监听scroll的滚动距离,注意用防抖(我用的是uView里自带的防抖方法),防止nav跳动

获取每个titile距离盒子顶部的距离,用于判断滚动距离是否超出某个分类

onReady(() => {list.value.forEach((item, index) => {uni.createSelectorQuery().select('#title' + index).boundingClientRect(data => {console.log(data);titleH.value.push(data)}).exec()})})

获取“this”:

const {appContext: {app: {config: {globalProperties}}}} = getCurrentInstance()
/* 菜单滚动监听 */function scroll(e) {//防抖globalProperties.$u.debounce(() => {console.log(e.detail.scrollTop);titleH.value.forEach((item, index) => {if ((e.detail.scrollTop + item.height) > item.top) {current.value = index}})scrollH.value = e.detail.scrollTop}, 100)}

3、加入购物车动画

购物车是固定的,我们得给它固定的id以便找到它

<view class="bottom"><view id="left_icon" class="left_icon" ref="cartBtn" @click="showPop = !showPop"><u-icon name="bag" size="80" color="#fff"></u-icon></view><view class="bottom_info"><view>共计:<text style="font-weight: bold;color: #FB3B26;">{{35}}</text>元</view><view>已点:早餐、中餐、晚餐</view></view><view class="submit">确认预订</view></view></view>

定义移动小球的样式,写活它的初始位置

<!-- 小球 --><view class="ball" v-if="showAnimation" :animation="animation":style="{ top: ballTop + 'px', left: ballLeft + 'px' }"></view>
.ball {position: absolute;z-index: 1;width: 40rpx;height: 40rpx;background-color: red;border-radius: 50%;}

写活“+”号的id,以便我们获取实例

<view v-if="id" class="plus" :id="id" @click="addClick"><u-icon :name="plusIcon" size="32" color="#ffffff" :customStyle="iconStyle"></u-icon></view>

 用uni.createAnimation()来制作动画,按钮的位置减去购物车的位置等于偏移的位置

/* 动画效果控制 */function addToCart(item) {const btn = '#id_' + item.id;const car = '.left_icon';console.log('#id_' + item.id);uni.createSelectorQuery().select(btn).boundingClientRect().exec((rect) => {const btnRect = rect[0];const left = btnRect.left;const top = btnRect.top;ballTop.value = top;ballLeft.value = left;uni.createSelectorQuery().select(car).boundingClientRect().exec((rect) => {console.log(rect);const carRect = rect[0];const x = carRect.left;const y = carRect.top;carTop.value = carRect.top;carLeft.value = carRect.left;animationData.value = uni.createAnimation()animationData.value.translate(x - left + 20, y - top).step({duration: 300,})animationTimeout.valueclearTimeout(animationTimeout.value)animation.value = animationData.value.export()showAnimation.value = true;animationTimeout.value = setTimeout(() => {showAnimation.value = false;}, 300);});});}

4、代码

页面booking.vue

<template><view class="booking"><view class="content"><view class="nav"><left-nav :data="list" :current="current" @change="navChange"></left-nav></view><scroll-view :scroll-into-view="toView" scroll-with-animation="true" class="main" @scroll="scroll" scroll-y><view class="scroll_main"><view class="" v-for="(item,index) in list" :id="'type' + index"><view :id="'title' + index"><u-divider>{{item.meal_name}}</u-divider></view><card v-for="(item2,indax) in item.goods" :data="item2" @change="cardChange"></card></view></view></scroll-view></view><view class="bottom"><view id="left_icon" class="left_icon" ref="cartBtn" @click="showPop = !showPop"><u-icon name="bag" size="80" color="#fff"></u-icon></view><view class="bottom_info"><view>共计:<text style="font-weight: bold;color: #FB3B26;">{{35}}</text>元</view><view>已点:早餐、中餐、晚餐</view></view><view class="submit">确认预订</view></view></view><!-- 弹出层 --><u-popup v-model="showPop" mode="bottom" border-radius="20" closeable z-index="1"><scroll-view class="pop_main" scroll-y><view class="pop_title">已选菜品</view><view class="scroll_main"><view class="" v-for="(item,index) in list" :id="'type' + index"><view :id="'title' + index"><u-divider>{{item.meal_name}}</u-divider></view><card v-for="(item2,indax) in item.goods" :data="item2" @change="cardChange" :isAdd="false"></card></view></view></scroll-view></u-popup><!-- 小球 --><view class="ball" v-if="showAnimation" :animation="animation":style="{ top: ballTop + 'px', left: ballLeft + 'px' }"></view>
</template><script setup>import leftNav from "@/components/booking/nav.vue"import card from "@/components/booking/card.vue"import {mockData} from "../binding/mock.js"import {getCurrentInstance,ref} from "vue";import {onLoad,onReady} from '@dcloudio/uni-app';onLoad(e => {mock.value = mockDatalist.value = mock.value.data.datasconsole.log(list.value);})onReady(() => {list.value.forEach((item, index) => {uni.createSelectorQuery().select('#title' + index).boundingClientRect(data => {console.log(data);titleH.value.push(data)}).exec()})})const showPop = ref(false)const animationData = ref()const animation = ref()const animationTimeout = ref()const titleH = ref([])const scrollH = ref(0)const toView = ref("")const current = ref(0)const mock = ref()const list = ref([{}])let ballTop = ref(0);let ballLeft = ref(0);let carTop = ref(0);let carLeft = ref(0);const showAnimation = ref(false);const {appContext: {app: {config: {globalProperties}}}} = getCurrentInstance()/* 菜单滚动监听 */function scroll(e) {//防抖globalProperties.$u.debounce(() => {console.log(e.detail.scrollTop);titleH.value.forEach((item, index) => {if ((e.detail.scrollTop + item.height) > item.top) {current.value = index}})scrollH.value = e.detail.scrollTop}, 100)}function cardChange(e) {addToCart(e)}function navChange(e) {current.value = egetRightScrollDistance()}function getRightScrollDistance() {toView.value = "title" + current.value;}/* 动画效果控制 */function addToCart(item) {const btn = '#id_' + item.id;const car = '.left_icon';console.log('#id_' + item.id);uni.createSelectorQuery().select(btn).boundingClientRect().exec((rect) => {const btnRect = rect[0];const left = btnRect.left;const top = btnRect.top;ballTop.value = top;ballLeft.value = left;uni.createSelectorQuery().select(car).boundingClientRect().exec((rect) => {console.log(rect);const carRect = rect[0];const x = carRect.left;const y = carRect.top;carTop.value = carRect.top;carLeft.value = carRect.left;animationData.value = uni.createAnimation()animationData.value.translate(x - left + 20, y - top).step({duration: 300,})animationTimeout.valueclearTimeout(animationTimeout.value)animation.value = animationData.value.export()showAnimation.value = true;animationTimeout.value = setTimeout(() => {showAnimation.value = false;}, 300);});});}
</script><style lang="scss" scoped>page {background-color: #fff;}.content {min-height: 100vh;display: flex;.nav {flex: 1;min-width: 164rpx;background-color: #F6F6F6;}.main {flex: 3.5;height: 100vh;background-color: #fff;.scroll_main {padding-bottom: 150rpx;}}}.bottom {position: absolute;z-index: 2;bottom: 0;width: 750rpx;height: 132rpx;background: #FFFFFF;box-shadow: 0rpx -2rpx 16rpx 2rpx rgba(164, 164, 164, 0.11);border-radius: 0rpx 0rpx 0rpx 0rpx;display: flex;justify-content: space-between;align-items: center;.bottom_info {flex: 1;margin: 0 20rpx;font-size: 26rpx;line-height: 40rpx;&>view:nth-child(2) {font-size: 24rpx;color: #aaa;}}.submit {color: #FFFFFF;padding: 10rpx 20rpx;background-color: #FB3B26;font-size: 26rpx;border-radius: 30rpx;margin-right: 50rpx;}#left_icon {margin-top: -30rpx;margin-left: 40rpx;width: 120rpx;height: 120rpx;background: #FB3B26;border-radius: 40rpx;line-height: 150rpx;text-align: center;}}.ball {position: absolute;z-index: 1;width: 40rpx;height: 40rpx;background-color: red;border-radius: 50%;}.pop_main {position: relative;max-height: 60vh;padding-top: 100rpx;padding-bottom: 150rpx;&>.pop_title {text-align: center;width: 100vw;height: 100rpx;font-size: 32rpx;font-weight: bold;position: fixed;top: 0;z-index: 1;background-color: #fff;line-height: 100rpx;text-align: center;}}
</style>

侧边栏组件nav.vue

<template><view class="nav_main"><view v-for="(item,index) in data" :class="{'tool-box':true,'item':true,'item_act':current==index}"@click="change(index)">{{item.meal_name}}</view></view>
</template><script setup>const emit = defineEmits(['change'])const props = defineProps({data: {type: Array,default: () => ([])},current: {type: Number,default: () => (0)},});function change(index) {emit('change', index) // 当前值 + 进步值}
</script><style scoped lang="scss">.nav_main {position: fixed;}.item {width: 164rpx;text-align: center;padding: 30rpx 0;font-size: 26rpx;color: #000000;font-weight: 400;position: relative;}.item_act {background-color: #fff;font-size: 26rpx;font-weight: 700;&::before {content: "";display: inline-block;width: 12rpx;height: 34rpx;background: #FC4E3E;border-radius: 0rpx 30rpx 30rpx 0rpx;position: absolute;left: 0;}}
</style>

商品卡片组件card.vue

<template><view class="card_body"><view class="image"></view><view class="foods_info"><view>{{data.name}}</view><view></view><view><view class="">¥{{data.price}}</view><counter v-if="isAdd" :id="'id_' + data.id" :number="data.number ?? 0" @change-click="change"></counter></view></view></view>
</template><script setup>import counter from "@/components/booking/counter.vue"const emit = defineEmits(['change'])const props = defineProps({data: {type: Object,default: () => ({})},isAdd: {type: Boolean,default: () => true}});function change(e) {let obj = props.dataobj.number = econsole.log(obj);emit('change', obj)}
</script><style scoped lang="scss">.card_body {display: flex;margin: 30rpx 20rpx;.image {width: 180rpx;height: 180rpx;background-color: #a1a1a1;border-radius: 10rpx;margin-right: 20rpx;}.foods_info {display: flex;flex-direction: column;justify-content: space-between;flex: 1;&>view:nth-child(1) {font-weight: 700;font-size: 28rpx;color: #000000;}&>view:nth-child(3) {display: flex;align-items: center;font-weight: 400;font-size: 32rpx;color: #000000;justify-content: space-between;}}}
</style>

进步器组件counter.vue

<template><view class="counter"><u-icon v-if="number>0" :name="reduceIcon" size="60" color="#8E8E8E" @click="reduceClick"></u-icon><input v-if="number>0" type="number" :value="number" @blur="inputBlurEvent" @input="inputChangeEvent":disabled="disabled"><view v-if="id" class="plus" :id="id" @click="addClick"><u-icon :name="plusIcon" size="32" color="#ffffff" :customStyle="iconStyle"></u-icon></view></view>
</template><script setup>import {ref,reactive,computed,nextTick} from "vue";const props = defineProps({id: {type: String,default: ""},disabled: {type: Number,default: false},number: {type: Number,default: 0},maxNumber: {type: Number,default: 99999},minNumber: {type: Number,default: 0},progressValue: {type: Number,default: 1},reduceIcon: {type: String,default: "minus-circle"},plusIcon: {type: String,default: "plus"}})const temp = computed(() => {return props.number})const iconStyle = reactive({fontWeight: 'blod'})const emit = defineEmits(['change-click'])// 加function addClick(ev) {emit('change-click', props.number + props.progressValue) // 当前值 + 进步值}// 减function reduceClick() {if (props.number <= props.minNumber) {console.log("不能继续减少啦 ~");return;}if ((props.number - props.progressValue) < props.minNumber) {console.log("不能继续减少");return;}// 3、执行 减操作emit('change-click', props.number - props.progressValue)}function inputBlurEvent(e) {let number = parseInt(e.detail.value)if (isNaN(number) || number === 0) {emit('change-click', 0)return;}// 条件:输入数不为进步值的倍数,则往前取成倍数值let multipie = Math.ceil(number / props.progressValue) // 获取倍数number = multipie * props.progressValue // 向上获取最近的倍数if (number > props.maxNumber) {number = props.maxNumberemit('change-click', number)} else if (number <= props.minNumber) {emit('change-click', props.minNumber)} else {emit('change-click', number)}}function inputChangeEvent(e) {// 限制输入在最大与最小值之间// 注意:因为都是赋值最大或最小值,所以会出现值复用无法重新渲染页面的情况(第一次能重新渲染,之后的都不渲染):已解决let number = parseInt(e.detail.value)if (isNaN(number) || number === 0) {// 为空为0return}if (number > props.maxNumber) {emit('change-click', props.maxNumber)} else if (number <= props.minNumber) {emit('change-click', props.minNumber)} else {emit('change-click', number)}}
</script><style lang="scss" scoped>.counter {display: flex;align-items: center;&>input {width: 2em;font-size: 28rpx;font-family: Source Han Sans CN-Bold, Source Han Sans CN;font-weight: bold;color: #000000;flex: 1;text-align: center;}.plus {margin: 8rpx;width: 48rpx;height: 48rpx;border-radius: 50%;background: #FF3232;display: flex;justify-content: center;align-items: center;&>image {width: 32rpx;height: 30rpx;margin-right: 5rpx;}}}
</style>

模拟数据mock.js

const mockData = {"code": 200,"msg": "","data": {"datas": [{"meal_id": 5,"meal_name": "早餐","meal_type": 1,"goods": [{"id": 4,"name": "牛奶","price": "3.00","img": ""},{"id": 5,"name": "馒头","price": "2.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/f82315767e959b536f64b0a199f99eb5.png"},{"id": 6,"name": "手抓饼","price": "6.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/9370838db9f50a2e950070995975e3b7.png"}]},{"meal_id": 5,"meal_name": "午餐","meal_type": 1,"goods": [{"id": 7,"name": "牛奶","price": "3.00","img": ""},{"id": 8,"name": "牛奶","price": "3.00","img": ""},{"id": 9,"name": "牛奶","price": "3.00","img": ""},{"id": 10,"name": "牛奶","price": "3.00","img": ""},{"id": 11,"name": "牛奶","price": "3.00","img": ""},{"id": 12,"name": "牛奶","price": "3.00","img": ""},{"id": 13,"name": "牛奶","price": "3.00","img": ""},{"id": 14,"name": "牛奶","price": "3.00","img": ""},{"id": 15,"name": "牛奶","price": "3.00","img": ""},{"id": 16,"name": "牛奶","price": "3.00","img": ""},{"id": 17,"name": "牛奶","price": "3.00","img": ""},{"id": 18,"name": "牛奶","price": "3.00","img": ""},{"id": 19,"name": "馒头","price": "2.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/f82315767e959b536f64b0a199f99eb5.png"},{"id": 20,"name": "手抓饼","price": "6.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/9370838db9f50a2e950070995975e3b7.png"}]},{"meal_id": 5,"meal_name": "晚餐","meal_type": 1,"goods": [{"id": 21,"name": "牛奶","price": "3.00","img": ""},{"id": 22,"name": "馒头","price": "2.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/f82315767e959b536f64b0a199f99eb5.png"},{"id": 23,"name": "手抓饼","price": "6.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/9370838db9f50a2e950070995975e3b7.png"}]},{"meal_id": 5,"meal_name": "宵夜","meal_type": 1,"goods": [{"id": 24,"name": "牛奶","price": "3.00","img": ""},{"id": 25,"name": "馒头","price": "2.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/f82315767e959b536f64b0a199f99eb5.png"},{"id": 26,"name": "手抓饼","price": "6.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/9370838db9f50a2e950070995975e3b7.png"}]}],"school_name": "测试学校"}
}export {mockData
}

这篇关于基于uniapp vue3.0 uView 做一个点单页面(包括加入购物车动画和左右联动)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/951415

相关文章

CSS3中使用flex和grid实现等高元素布局的示例代码

《CSS3中使用flex和grid实现等高元素布局的示例代码》:本文主要介绍了使用CSS3中的Flexbox和Grid布局实现等高元素布局的方法,通过简单的两列实现、每行放置3列以及全部代码的展示,展示了这两种布局方式的实现细节和效果,详细内容请阅读本文,希望能对你有所帮助... 过往的实现方法是使用浮动加

css渐变色背景|<gradient示例详解

《css渐变色背景|<gradient示例详解》CSS渐变是一种从一种颜色平滑过渡到另一种颜色的效果,可以作为元素的背景,它包括线性渐变、径向渐变和锥形渐变,本文介绍css渐变色背景|<gradien... 使用渐变色作为背景可以直接将渐China编程变色用作元素的背景,可以看做是一种特殊的背景图片。(是作为背

CSS自定义浏览器滚动条样式完整代码

《CSS自定义浏览器滚动条样式完整代码》:本文主要介绍了如何使用CSS自定义浏览器滚动条的样式,包括隐藏滚动条的角落、设置滚动条的基本样式、轨道样式和滑块样式,并提供了完整的CSS代码示例,通过这些技巧,你可以为你的网站添加个性化的滚动条样式,从而提升用户体验,详细内容请阅读本文,希望能对你有所帮助...

css实现图片旋转功能

《css实现图片旋转功能》:本文主要介绍了四种CSS变换效果:图片旋转90度、水平翻转、垂直翻转,并附带了相应的代码示例,详细内容请阅读本文,希望能对你有所帮助... 一 css实现图片旋转90度.icon{ -moz-transform:rotate(-90deg); -webkit-transfo

vue基于ElementUI动态设置表格高度的3种方法

《vue基于ElementUI动态设置表格高度的3种方法》ElementUI+vue动态设置表格高度的几种方法,抛砖引玉,还有其它方法动态设置表格高度,大家可以开动脑筋... 方法一、css + js的形式这个方法需要在表格外层设置一个div,原理是将表格的高度设置成外层div的高度,所以外层的div需要

Vue项目中Element UI组件未注册的问题原因及解决方法

《Vue项目中ElementUI组件未注册的问题原因及解决方法》在Vue项目中使用ElementUI组件库时,开发者可能会遇到一些常见问题,例如组件未正确注册导致的警告或错误,本文将详细探讨这些问题... 目录引言一、问题背景1.1 错误信息分析1.2 问题原因二、解决方法2.1 全局引入 Element

详解如何在React中执行条件渲染

《详解如何在React中执行条件渲染》在现代Web开发中,React作为一种流行的JavaScript库,为开发者提供了一种高效构建用户界面的方式,条件渲染是React中的一个关键概念,本文将深入探讨... 目录引言什么是条件渲染?基础示例使用逻辑与运算符(&&)使用条件语句列表中的条件渲染总结引言在现代

详解Vue如何使用xlsx库导出Excel文件

《详解Vue如何使用xlsx库导出Excel文件》第三方库xlsx提供了强大的功能来处理Excel文件,它可以简化导出Excel文件这个过程,本文将为大家详细介绍一下它的具体使用,需要的小伙伴可以了解... 目录1. 安装依赖2. 创建vue组件3. 解释代码在Vue.js项目中导出Excel文件,使用第三

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

Window Server2016加入AD域的方法步骤

《WindowServer2016加入AD域的方法步骤》:本文主要介绍WindowServer2016加入AD域的方法步骤,包括配置DNS、检测ping通、更改计算机域、输入账号密码、重启服务... 目录一、 准备条件二、配置ServerB加入ServerA的AD域(test.ly)三、查看加入AD域后的变